自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

安全函數(shù)不安全-多線程慎用List.h

系統(tǒng) Linux
linux 開發(fā)應該多少都聽過大名鼎鼎的 list.h ,其簡潔優(yōu)雅的設計,一個頭文件完成了一個高可用的鏈表。

[[440988]]

 本文轉載自微信公眾號「非典型技術宅」,作者無知少年。轉載本文請聯(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)。

  1. typedef struct product_s 
  2.     struct list_head product_node; 
  3.     uint32_t index
  4.     uint32_t weight;   
  5. }product_t; 
  6.  
  7. //初始化鏈表頭 
  8. LIST_HEAD(product_list); 

生產(chǎn)者在生產(chǎn)完產(chǎn)品后,將產(chǎn)品加入鏈表,等待消費者使用。

  1. void producer(void) 
  2.     product_t *product = malloc(sizeof(product_t)); 
  3.  
  4.     // 產(chǎn)品重量為 300 ± 10 
  5.     product->weight = 290 + rand() % 20; 
  6.      
  7.     printf("product :%p, weight %d\n", product, product->weight); 
  8.     list_add_tail(&product->product_node, &product_list); 

遍歷鏈表

使用 list_for_each_entry 可以將鏈表進行遍歷:

  1. // 遍歷打印鏈表信息 
  2. void print_produce_list(void) 
  3.     product_t *product; 
  4.     list_for_each_entry(product, &product_list, product_node) 
  5.     { 
  6.         printf("manufacture product :%p, weight %d\n", product, product->weight); 
  7.     } 

其具體實現(xiàn)是使用宏將 for 循環(huán)的初始條件和完成條件進行了替換:

  1. #define list_for_each_entry(pos, head, member)                \ 
  2.     for (pos = list_first_entry(head, typeof(*pos), member);    \ 
  3.          &pos->member != (head);                    \ 
  4.          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ù)

  1. // 刪除重量小于300的節(jié)點 
  2. void remove_unqualified_produce(void) 
  3.     product_t *product, *temp
  4.     list_for_each_entry_safe(product, temp, &product_list, product_node) 
  5.     { 
  6.         // 移除重量小于300的產(chǎn)品 
  7.         if (product->weight < 300) 
  8.         { 
  9.             printf("remove product :%p, weight %d\n", product, product->weight); 
  10.             list_del(&product->product_node); 
  11.             free(product); 
  12.         } 
  13.     } 

其實現(xiàn)是使用一個中間變量,在開始每次開始執(zhí)行循環(huán)體前,將當前節(jié)點的下一個節(jié)點保存到中間變量,從而實現(xiàn)"安全"遍歷

  1. #define list_for_each_entry_safe(pos, n, head, member)            \ 
  2.     for (pos = list_first_entry(head, typeof(*pos), member),    \ 
  3.         n = list_next_entry(pos, member);            \ 
  4.          &pos->member != (head);                    \ 
  5.          pos = n, n = list_next_entry(n, member)) 

多線程中使用list.h

上面我們在主線程里面創(chuàng)建了產(chǎn)品,并放入到鏈表中并,并過濾了重量小于300的產(chǎn)品。

后面我們在多線程中對產(chǎn)品進行消費(兩個線程同時消費鏈表的數(shù)據(jù),使用完成后刪除并釋放結點)。

這里的邏輯和單線程中的差不多,同樣是遍歷鏈表,然后從鏈表中刪除節(jié)點。不同的是,由于list.h自身沒有帶鎖,所以需要使用互斥鎖將鏈表的操作進行保護。

于是很自然的有了下面的代碼

  1. void * consumer(void *arg) 
  2.     product_t *product, *temp
  3.  
  4.     // 使用互斥鎖對鏈表進行保護 
  5.     pthread_mutex_lock(&producer_mutex); 
  6.     list_for_each_entry_safe(product, temp, &product_list, product_node) 
  7.     { 
  8.         list_del(&product->product_node); 
  9.         printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self()); 
  10.         pthread_mutex_unlock(&producer_mutex); 
  11.  
  12.         // 睡一會,防止太快了 
  13.         usleep(10*1000); 
  14.         free(product); 
  15.         pthread_mutex_lock(&producer_mutex); 
  16.     } 
  17.     pthread_mutex_unlock(&producer_mutex); 
  18.      
  19.     return NULL

在上面的這段代碼中,在對鏈表操作時,使用互斥鎖對鏈表進行了保護,使同時只能有一個線程訪問鏈表。

不過你以為這樣就好了嘛,如果時這樣,這篇文章就沒存在的必要了。。。

[[440989]]

在兩個線程同時遍歷時,即便是加了鎖之后,數(shù)據(jù)訪問也不安全。

在遍歷使用的 list_for_each_entry_safe 宏中,使用了一個零時變量對保存著當前鏈表的下一個節(jié)點。

但是多線程訪問鏈表時,有可能零時變量保存的節(jié)點,被另一個線程刪除了,所以訪問的時候又是 Segmentation fault

[[440990]]

后記

原因找到了,也就好辦了。以至于解決方法嘛,我是使用一個全局的零時變量,將需要刪除節(jié)點的下一個節(jié)點保存起來,手動實現(xiàn)多線程的"安全刪除"。

  1. // 消費者 
  2. void * consumer(void *arg) 
  3.     product_t *product; 
  4.  
  5.     // 使用互斥鎖對鏈表進行保護 
  6.     pthread_mutex_lock(&producer_mutex); 
  7.     list_for_each_entry(product, &product_list, product_node) 
  8.     { 
  9.         temp = list_next_entry(product, product_node); 
  10.         list_del(&product->product_node); 
  11.         printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self()); 
  12.         pthread_mutex_unlock(&producer_mutex); 
  13.  
  14.         // 睡一會,防止太快 
  15.         usleep(10*1000); 
  16.         free(product); 
  17.  
  18.         pthread_mutex_lock(&producer_mutex); 
  19.         if(temp != NULL){ 
  20.             product = list_prev_entry(temp, product_node); 
  21.         } 
  22.     } 
  23.     pthread_mutex_unlock(&producer_mutex); 
  24.  
  25.     return NULL

一個晚上找到了這個bug,然后又花了一個晚上記錄下來這個bug。

據(jù)說 klist.h 是 list.h 的線程安全版本,后面花時間在研究一下去,今天就先睡了。。。

 

責任編輯:武曉燕 來源: 非典型技術宅
相關推薦

2012-04-16 10:12:54

Java線程

2024-01-19 08:42:45

Java線程字符串

2020-04-22 20:35:02

HashMap線程安全

2015-07-01 14:48:51

2014-09-12 17:44:23

2024-03-22 12:29:03

HashMap線程

2009-08-03 16:58:59

C#不安全代碼

2014-04-09 09:37:29

2021-05-17 07:51:44

SimpleDateF線程安全

2017-02-16 08:50:00

2023-06-01 19:24:16

2020-11-03 12:32:25

影子物聯(lián)網(wǎng)物聯(lián)網(wǎng)IOT

2021-04-04 23:16:52

安全刷臉銀行

2015-05-27 16:13:05

2010-08-16 10:01:01

2021-12-08 07:31:40

Linux安全病毒

2009-11-18 10:05:13

2018-01-26 10:49:19

2023-09-18 08:01:06

Spring管理Mybatis

2009-11-12 08:38:34

點贊
收藏

51CTO技術棧公眾號