安全函數(shù)不安全-多線程慎用List.h
本文轉載自微信公眾號「非典型技術宅」,作者無知少年。轉載本文請聯(lián)系非典型技術宅公眾號。
前言
linux 開發(fā)應該多少都聽過大名鼎鼎的 list.h ,其簡潔優(yōu)雅的設計,一個頭文件完成了一個高可用的鏈表。
但是 list.h 并不是線程安全的,在多線程的情況下使用,必須考慮多線程數(shù)據(jù)同步的問題。
然而。。。。
我在使用互斥鎖對鏈表的操作進行保護之后,還是被坑了!
下面是把我坑了的 list_for_each_entry 和 list_for_each_entry_safe 兩個函數(shù)的詳細分析。
list.h 單線程使用
在 list.h 這個文件中有非常多值得學習的地方,比如其最經(jīng)典的 container_of 的實現(xiàn)。
在這里只介紹幾個常用的函數(shù),然后重點分析在多線程使用時的碰到的坑。
鏈表初始化及添加節(jié)點
首先定義一個鏈表和鏈表節(jié)點,定義一個產(chǎn)品,其屬性為產(chǎn)品重量(weight)。
- typedef struct product_s
- {
- struct list_head product_node;
- uint32_t index;
- uint32_t weight;
- }product_t;
- //初始化鏈表頭
- LIST_HEAD(product_list);
生產(chǎn)者在生產(chǎn)完產(chǎn)品后,將產(chǎn)品加入鏈表,等待消費者使用。
- void producer(void)
- {
- product_t *product = malloc(sizeof(product_t));
- // 產(chǎn)品重量為 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);
- }
- }
其具體實現(xiàn)是使用宏將 for 循環(huán)的初始條件和完成條件進行了替換:
- #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循環(huán)的第一個參數(shù)將 pos = list_first_entry(head, typeof(*pos), member); 初始化為鏈表頭指向的第一個實體鏈表成員。
第二個參數(shù) &pos->member != (head) 為跳出條件,當pos->member再次指向鏈表頭時跳出for循環(huán)。
for的第三個參數(shù)通過pos->member.next指針遍歷整個實體鏈表,當pos->member.next再次指向我們的鏈表頭的時候跳出for循環(huán)。
但是 list_for_each_entry 不能在遍歷的循環(huán)體中刪除節(jié)點,因為在循環(huán)體中刪除鏈表節(jié)點后,當前節(jié)點的前驅結點和后繼結點指針會被置空。
在for循環(huán)的第三個參數(shù)中,獲取下一個節(jié)點時,會發(fā)生非法指針訪問
“安全遍歷鏈表”
為了解決在遍歷鏈表過程中,無法刪除結點的問題,在 list.h 中提供了一個安全刪除節(jié)點的函數(shù)
- // 刪除重量小于300的節(jié)點
- void remove_unqualified_produce(void)
- {
- product_t *product, *temp;
- list_for_each_entry_safe(product, temp, &product_list, product_node)
- {
- // 移除重量小于300的產(chǎn)品
- if (product->weight < 300)
- {
- printf("remove product :%p, weight %d\n", product, product->weight);
- list_del(&product->product_node);
- free(product);
- }
- }
- }
其實現(xiàn)是使用一個中間變量,在開始每次開始執(zhí)行循環(huán)體前,將當前節(jié)點的下一個節(jié)點保存到中間變量,從而實現(xiàn)"安全"遍歷
- #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))
多線程中使用list.h
上面我們在主線程里面創(chuàng)建了產(chǎn)品,并放入到鏈表中并,并過濾了重量小于300的產(chǎn)品。
后面我們在多線程中對產(chǎn)品進行消費(兩個線程同時消費鏈表的數(shù)據(jù),使用完成后刪除并釋放結點)。
這里的邏輯和單線程中的差不多,同樣是遍歷鏈表,然后從鏈表中刪除節(jié)點。不同的是,由于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;
- }
在上面的這段代碼中,在對鏈表操作時,使用互斥鎖對鏈表進行了保護,使同時只能有一個線程訪問鏈表。
不過你以為這樣就好了嘛,如果時這樣,這篇文章就沒存在的必要了。。。
在兩個線程同時遍歷時,即便是加了鎖之后,數(shù)據(jù)訪問也不安全。
在遍歷使用的 list_for_each_entry_safe 宏中,使用了一個零時變量對保存著當前鏈表的下一個節(jié)點。
但是多線程訪問鏈表時,有可能零時變量保存的節(jié)點,被另一個線程刪除了,所以訪問的時候又是 Segmentation fault
后記
原因找到了,也就好辦了。以至于解決方法嘛,我是使用一個全局的零時變量,將需要刪除節(jié)點的下一個節(jié)點保存起來,手動實現(xiàn)多線程的"安全刪除"。
- // 消費者
- 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。
據(jù)說 klist.h 是 list.h 的線程安全版本,后面花時間在研究一下去,今天就先睡了。。。