系統程式設計師成長計畫 併發 五

2021-05-25 19:19:04 字數 3562 閱讀 3295

文章出處:

作者****:李先靜

無鎖(lock-free)資料結構

多執行緒併發執行時,雖然有共享資料,如果所有執行緒只是讀取共享資料而不修改它,也是不用加鎖的,比如**段就是共享的「資料」,每個執行緒都會讀取, 但是不用加鎖。排除所有這些情況,多執行緒之間有共享資料,有的執行緒要修改這些共享資料,有的執行緒要讀取這些共享資料,這才是程式設計師需要關注的情況,也是本 節我們討論的範圍。

在併發的環境裡,加鎖可以保護共享的資料,但是加鎖也會存在一些問題:

o 由於臨界區無法併發執行,進入臨界區就需要等待,加鎖帶來效率的降低。

o 在複雜的情況下,很容易造成死鎖,併發實體之間無止境的互相等待。

o 在中斷/訊號處理函式中不能加鎖,給併發處理帶來困難。

o 優先順序倒置造成實時系統不能正常工作。低階優先程序拿到高優先順序程序需要的鎖,結果是高/低優先順序的程序都無法執行,中等優先順序的程序可能在狂跑。

由於併發與加鎖(互斥)的矛盾關係,無鎖資料結構自然成為程式設計師關注的焦點,這也是本節要介紹的:

o cpu提供的原子操作。

大約在七八年前,我們用apache的xerces來解析xml檔案,奇怪的是多執行緒反而比單執行緒慢。他們找了很久也沒有找出原因,只是證實使用多 程序代替多執行緒會快乙個數量級,在windows上他們就使用了多程序的方式。後來移植到linux時候,我發現xerces每建立乙個結點都會去更新一 些全域性的統計資訊,比如把結點的總數加一,它使用的pthread_mutex實現互斥。這就是問題所在:乙個xml文件有數以萬計的結點,以50個執行緒 併發為例,每個執行緒解析乙個xml文件,總共要進行上百萬次的加鎖/解鎖,幾乎所有執行緒都在等待,你說能快得了嗎?

當時我知道windows下有interlockedincrement之類的函式,它們利用cpu一些特殊指令,保證對整數的基本操作是原子的。 查詢了一些資源發現linux下也有類似的函式,後來我把所有加鎖去掉,換成這些原子操作,速度比多程序執行還快了幾倍。下面我們看++和—的原子操作在 ia架構上的實現:

#define atomic_smp_lock "lock ; "

typedef struct atomic_t;

static __inline__ void atomic_inc(atomic_t *v)

static __inline__ void atomic_dec(atomic_t *v)

o 單入單出的迴圈佇列。單入單出的迴圈佇列是一種特殊情況,雖然特殊但是很實用,重要的是它不需要加鎖。這裡的單入是指只有乙個執行緒向佇列裡追加資料 (push),單出只是指只有乙個執行緒從佇列裡取資料(pop),迴圈佇列與普通佇列相比,不同之處在於它的最大資料儲存量是事先固定好的,不能動態增 長。儘管有這些限制它的應用還是相當廣泛的。這我們介紹一下它的實現:

資料下定義如下:

typedef struct _fiforing

fiforing;

r_cursor指向佇列頭,用於取資料(pop)。w_cursor指向佇列尾,用於追加資料(push)。length表示佇列的最大資料儲存量,data表示存放的資料,[0]在這裡表示變長的緩衝區(前面我們已經講過)。

建立函式

fiforing* fifo_ring_create(size_t length)

return thiz;

}

這裡我們要求佇列的長度大於1而不是大於0,為什麼呢?排除長度為1的佇列沒有什麼意義的原因外,更重要的原因是佇列頭與佇列尾重疊 (r_cursor= =w_cursor) 時,到底表示是滿佇列還是空佇列?這個要搞清楚才行,上次乙個同事犯了這個錯誤,讓我們查了很久。這裡我們認為佇列頭與佇列尾重疊時表示隊列為空,這與隊 列初始狀態一致,後面在寫的時候始終保留乙個空位,避免佇列頭與佇列尾重疊,這樣可以消除歧義了。

追加資料(push)

ret fifo_ring_push(fiforing* thiz, void* data)

return ret;

}

佇列頭和佇列尾之間還有乙個以上的空位時就追加資料,否則返回失敗。

取資料(pop)

ret fifo_ring_pop(fiforing* thiz, void** data)

return ret;

}

佇列頭和佇列尾不重疊表示佇列不為空,取資料並移動佇列頭。

o 單寫多讀的無鎖資料結構。單寫表示只有乙個執行緒去修改共享資料結構,多讀表示有多個執行緒去讀取共享資料結構。前面介紹的讀寫鎖可以有效的解決這個問題,但更高效的辦法是使用無鎖資料結構。思路如下:

就像為了避免顯示閃爍而使用的雙緩衝一樣,我們使用兩份資料結構,乙份資料結構用於讀取,所有執行緒都可以在不加鎖的情況下讀取這個資料結構。另外乙份資料結構用於修改,由於只有乙個執行緒會修改它,所以也不用加鎖。

在修改之後,我們再交換讀/寫的兩個函式結構,把另外乙份也修改過來,這樣兩個資料結構就一致了。在交換時要保證沒有執行緒在讀取,所以我們還需要乙個讀執行緒的引用計數。現在我們看看怎麼把前面寫的雙向鍊錶改為單寫多讀的無鎖資料結構。

為了保證交換是原子的,我們需要乙個新的原子操作cas(compare and swap)。

#define cas(_a, _o, _n)                                    /

()

資料結構

typedef struct _swmrdlist

swmrdlist;

兩個鍊錶,乙個用於讀乙個用於寫。rd_index_and_ref的最高位元組記錄用於讀取的雙向鍊錶的索引,低24位用於記錄讀取執行緒的引用記數,最大支援16777216個執行緒同時讀取,應該是足夠了,所以後面不考慮它的溢位。

讀取操作

int      swmr_dlist_find(swmrdlist* thiz, dlistdatacomparefunc cmp, void* ctx)

修改操作

ret swmr_dlist_insert(swmrdlist* thiz, size_t index, void* data)

while(cas(&(thiz->rd_index_and_ref), rd_index_old, rd_index_new));

wr_index = rd_index_old>>24;

ret = dlist_insert(thiz->dlists[wr_index], index, data);

}return ret;

}

先修改用於修改的雙向鍊錶,修改完成之後等到沒有執行緒讀取時,交換讀/寫兩個鍊錶,再修改另乙個鍊錶,此時兩個鍊錶狀態保持一致。

稍做改進,對修改的操作進行加鎖,就可以支援多讀多寫的資料結構,讀是無鎖的,寫是加鎖的。

o 真正的無鎖資料結構。andrei alexandrescu的《lock-freedatastructures》估計是這方面最經典的**了,對他的方法我開始感到驚奇後來感到失望,驚 奇的是演算法的巧妙,失望的是無鎖的限制和代價。作者最後說這種資料結構只適用於wrrmbntm(write-rarely-read-many -but-not-too-many)的情況。而且每次修改都要拷貝整個資料結構(甚至多次),所以不要指望這種方法能帶來多少效能上的提高,唯一的好處 是能避免加鎖帶來的部分***。有興趣的朋友可以看下這篇**,這裡我就不重複了。

系統程式設計師成長計畫 併發 五

無鎖 lock free 資料結構 多執行緒併發執行時,雖然有共享資料,如果所有執行緒只是讀取共享資料而不修改它,也是不用加鎖的,比如 段就是共享的 資料 每個執行緒都會讀取,但是不用加鎖。排除所有這些情況,多執行緒之間有共享資料,有的執行緒要修改這些共享資料,有的執行緒要讀取這些共享資料,這才是程...

系統程式設計師成長計畫005

1.這個變成大寫的函式,就不需要用函式指標來給foreach做引數了。因為他沒有什麼其他變種,不像print那樣,既要print int又要print str。函式指標,或者說 函式,別瞎用!2.書裡的寫法 dlist foreach dlist,str toupper,null 看來還是堅持了 函...

系統程式設計師成長計畫 併發 一 下

作者 李先靜 linux下的多執行緒程式設計使用pthread posix thread 函式庫,使用時包含標頭檔案pthread.h,鏈結共享庫libpthread.so。這裡順便說一下gcc鏈結共享庫的方式 l用來指 定共享庫所在目錄,系統庫目錄不用指定。l用來指定要鏈結的共享庫,只需要指定庫的...