安全函式不安全-多執行緒慎用List.h
linux 開發應該多少都聽過大名鼎鼎的 list.h ,其簡潔優雅的設計,一個標頭檔案完成了一個高可用的連結串列。
但是 list.h 並不是執行緒安全的,在多執行緒的情況下使用,必須考慮多執行緒資料同步的問題。
然而。。。。
我在使用互斥鎖對連結串列的操作進行保護之後,還是被坑了!
下面是把我坑了的 list_for_each_entry 和 list_for_each_entry_safe 兩個函式的詳細分析。
在 list.h 這個檔案中有非常多值得學習的地方,比如其最經典的 container_of 的實現。
在這裡只介紹幾個常用的函式,然後重點分析在多執行緒使用時的碰到的坑。
首先定義一個連結串列和連結串列節點,定義一個產品,其屬性為產品重量(weight)。
typedef struct product_s { struct list_head product_node; uint32_t index; uint32_t weight; }product_t; //初始化連結串列頭 LIST_HEAD(product_list);
生產者在生產完產品後,將產品加入連結串列,等待消費者使用。
void producer(void) { product_t *product = malloc(sizeof(product_t)); // 產品重量為 300 ± 10 product->weight = 290 + rand() % 20; printf("product :%p, weight %d\n", product, product->weight); list_add_tail(&product->product_node, &product_list); }
使用 list_for_each_entry 可以將連結串列進行遍歷:
// 遍歷列印連結串列資訊 void print_produce_list(void) { product_t *product; list_for_each_entry(product, &product_list, product_node) { printf("manufacture product :%p, weight %d\n", product, product->weight); } }
其具體實現是使用宏將 for 迴圈的初始條件和完成條件進行了替換:
#define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ &pos->member != (head); \ pos = list_next_entry(pos, member))
其中for迴圈的第一個引數將 pos = list_first_entry(head, typeof(*pos), member); 初始化為連結串列頭指向的第一個實體連結串列成員。
第二個引數 &pos->member != (head) 為跳出條件,當pos->member再次指向連結串列頭時跳出for迴圈。
for的第三個引數透過pos->member.next指標遍歷整個實體連結串列,當pos->member.next再次指向我們的連結串列頭的時候跳出for迴圈。
但是 list_for_each_entry 不能在遍歷的迴圈體中刪除節點,因為在迴圈體中刪除連結串列節點後,當前節點的前驅結點和後繼結點指標會被置空。
在for迴圈的第三個引數中,獲取下一個節點時,會發生非法指標訪問
為了解決在遍歷連結串列過程中,無法刪除結點的問題,在 list.h 中提供了一個安全刪除節點的函式
// 刪除重量小於300的節點 void remove_unqualified_produce(void) { product_t *product, *temp; list_for_each_entry_safe(product, temp, &product_list, product_node) { // 移除重量小於300的產品 if (product->weight < 300) { printf("remove product :%p, weight %d\n", product, product->weight); list_del(&product->product_node); free(product); } } }
其實現是使用一箇中間變數,在開始每次開始執行迴圈體前,將當前節點的下一個節點儲存到中間變數,從而實現"安全"遍歷
#define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member), \ n = list_next_entry(pos, member); \ &pos->member != (head); \ pos = n, n = list_next_entry(n, member))
上面我們在主執行緒裡面建立了產品,並放入到連結串列中並,並過濾了重量小於300的產品。
後面我們在多執行緒中對產品進行消費(兩個執行緒同時消費連結串列的資料,使用完成後刪除並釋放結點)。
這裡的邏輯和單執行緒中的差不多,同樣是遍歷連結串列,然後從連結串列中刪除節點。不同的是,由於list.h自身沒有帶鎖,所以需要使用互斥鎖將連結串列的操作進行保護。
於是很自然的有了下面的程式碼
void * consumer(void *arg) { product_t *product, *temp; // 使用互斥鎖對連結串列進行保護 pthread_mutex_lock(&producer_mutex); list_for_each_entry_safe(product, temp, &product_list, product_node) { list_del(&product->product_node); printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self()); pthread_mutex_unlock(&producer_mutex); // 睡一會,防止太快了 usleep(10*1000); free(product); pthread_mutex_lock(&producer_mutex); } pthread_mutex_unlock(&producer_mutex); return NULL; }
在上面的這段程式碼中,在對連結串列操作時,使用互斥鎖對連結串列進行了保護,使同時只能有一個執行緒訪問連結串列。
不過你以為這樣就好了嘛,如果時這樣,這篇文章就沒存在的必要了。。。
在兩個執行緒同時遍歷時,即便是加了鎖之後,資料訪問也不安全。
在遍歷使用的 list_for_each_entry_safe 宏中,使用了一個零時變數對儲存著當前連結串列的下一個節點。
但是多執行緒訪問連結串列時,有可能零時變數儲存的節點,被另一個執行緒刪除了,所以訪問的時候又是 Segmentation fault
原因找到了,也就好辦了。以至於解決方法嘛,我是使用一個全域性的零時變數,將需要刪除節點的下一個節點儲存起來,手動實現多執行緒的"安全刪除"。
// 消費者 void * consumer(void *arg) { product_t *product; // 使用互斥鎖對連結串列進行保護 pthread_mutex_lock(&producer_mutex); list_for_each_entry(product, &product_list, product_node) { temp = list_next_entry(product, product_node); list_del(&product->product_node); printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self()); pthread_mutex_unlock(&producer_mutex); // 睡一會,防止太快 usleep(10*1000); free(product); pthread_mutex_lock(&producer_mutex); if(temp != NULL){ product = list_prev_entry(temp, product_node); } } pthread_mutex_unlock(&producer_mutex); return NULL; }
一個晚上找到了這個bug,然後又花了一個晚上記錄下來這個bug。
據說 klist.h 是 list.h 的執行緒安全版本,後面花時間在研究一下去,今天就先睡了。。。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901823/viewspace-2849484/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 多執行緒安全strtok函式MStrTok執行緒函式
- 執行緒安全和執行緒不安全理解執行緒
- Java執行緒(一):執行緒安全與不安全Java執行緒
- go 1.9 多執行緒安全MAP 函式模組Go執行緒函式
- 什麼是執行緒安全和執行緒不安全執行緒
- 多執行緒常用函式執行緒函式
- 造成類在多執行緒時不安全的原因執行緒
- libcurl多執行緒超時設定不安全執行緒
- HashMap為何執行緒不安全HashMap執行緒
- ArrayList 為什麼執行緒不安全執行緒
- 多執行緒系列之 執行緒安全執行緒
- iOS 多執行緒之執行緒安全iOS執行緒
- iOS多執行緒之執行緒安全iOS執行緒
- Swift實現多執行緒map函式Swift執行緒函式
- SimpleDateFormat一定是執行緒不安全嗎?ORM執行緒
- 什麼時候執行緒不安全?怎樣做到執行緒安全?怎麼擴充套件執行緒安全的類?執行緒套件
- 【Java多執行緒】執行緒安全的集合Java執行緒
- 多執行緒安全(一)執行緒
- 【多執行緒總結(二)-執行緒安全與執行緒同步】執行緒
- Java 多執行緒基礎(四)執行緒安全Java執行緒
- iOS多執行緒安全-13種執行緒鎖?iOS執行緒
- 小度分享-【多執行緒工作及執行緒安全】執行緒
- 多執行緒與高併發(二)執行緒安全執行緒
- 併發與多執行緒之執行緒安全篇執行緒
- 深入理解 Java 多執行緒、Lambda 表示式及執行緒安全最佳實踐Java執行緒
- 多執行緒,你覺得你安全了?(執行緒安全問題)執行緒
- ArrayList執行緒不安全怎麼辦?(CopyOnWriteArrayList詳解)執行緒
- HashMap1.7與1.8執行緒不安全講解HashMap執行緒
- Java多執行緒中執行緒安全與鎖問題Java執行緒
- day20_多執行緒入門丶執行緒安全執行緒
- 多執行緒-以前的執行緒安全的類回顧執行緒
- GCD 多執行緒安全 單寫多讀GC執行緒
- 29-HashMap 為什麼是執行緒不安全的?HashMap執行緒
- [短文速讀 -5] 多執行緒程式設計引子:程式、執行緒、執行緒安全執行緒程式設計
- 多執行緒和多執行緒同步執行緒
- 多執行緒CreateThread函式的用法及注意事項執行緒thread函式
- 多執行緒【執行緒池】執行緒
- 多執行緒--執行緒管理執行緒