lockFreeQueue 無鎖佇列實現與總結

2022-05-03 21:51:10 字數 2381 閱讀 2356

在工程上,為了解決兩個處理器互動速度不一致的問題,我們使用佇列作為快取,生產者將資料放入佇列,消費者從佇列中取出資料。這個時候就會出現四種情況,單生產者單消費者,多生產者單消費者,單生成者多消費者,多生產者多消費者。我們知道,多執行緒往往會帶來資料不一致的情況,一般需要靠加鎖解決問題。但是,加鎖往往帶來阻塞,阻塞會帶來執行緒切換開銷,在資料量大的情況下鎖帶來的開銷是很大的,因此無鎖佇列實現勢在必行。下面就詳細講一下每種情況的不同實現方法。

從最簡單的單生產者單消費者說起,假設我們現在有乙個正常的佇列,寫執行緒往這個佇列的head處push資料,讀執行緒往這個佇列的tail處pop資料。試想,如果head和tail不相同,也就是兩個執行緒操作的資料不是同乙個,這個時候是不會產生衝突的,這意味著我們只需要在head == tail的時候做處理。這時候可以發現,head == tail的時候,正是佇列空的時候,也就是說這個時候讀執行緒是讀不到資料的,因此,讀執行緒和寫執行緒是不會產生衝突的,所以實現參考如下偽**

bool push(t &data) 

lockfreequeue[tail] =data;

//注意編譯器或者cpu可能會為了提公升效能將**亂序,為了上下兩句**不顛倒順序,這裡需要加上記憶體屏障

tail++;

return

true;}

bool pop(t &data)

data =lockfreequeue[head];

//記憶體屏障

head++;

return

true

;}

參考單生產者單消費者模型我們可以發現,讀和寫本身是無競爭的,競爭的是讀和讀之間,寫和寫之間。考慮單生產者多消費者問題,有沒有方式避免讀和讀之間的競爭呢?

解決競爭最粗暴的方法,直接上鎖。

可以利用cas模擬加鎖解鎖,定義乙個變數當鎖,然後將該變數置為0或者1代表未上鎖和已上鎖狀態,因為操作是原子的,所以保證每次只有乙個執行緒可以搶占到鎖。c提供了乙個__sync_bool_compare_and_swap介面使用,c++也有相關原子變數庫。這種加鎖解鎖非常快,適合頻繁加鎖解鎖的場景

如果真的不想加鎖,最簡單的方法就是,我根據讀執行緒數分配佇列數,也就說乙個讀執行緒對應乙個佇列,這樣,讀和讀就可以分離開來,也就不存在競爭了。然後寫依此去讀每個佇列,這樣可以達到完全無鎖。但是這會帶來其他問題,如果某個執行緒處理比較慢,他繫結的佇列裡的資訊無法及時清空,其他執行緒也無法幫他清空,可能會帶來某些資訊無法快速處理,導致超時。所以要根據實際情況選用這種模式。

可以發現,將佇列分成多個的做法不是完全不可取的。在只有乙個佇列的情況下,所有讀執行緒都會去競爭隊頭,在同一時間只會有乙個執行緒可以搶到鎖,然後進行讀,再釋放鎖。如果我現在有多個佇列,然後每個執行緒都可以讀任意乙個佇列,當然在讀之前還是要先搶占佇列使用權,但是因為佇列數量很多,可以把衝突盡可能分散掉。如果此時佇列數量足夠多,衝突率會更低。

但是這又帶來乙個問題,如何給執行緒分配佇列?最簡單的方法是輪詢,也就是每個執行緒一開始都在0號佇列處,依次嘗試去獲取每個佇列的使用權,搶不到的話就去獲取下乙個。這種做法簡單粗暴,也可以很明顯地看出衝突率十分高,因為每次都是取下乙個佇列,一旦乙個佇列搶占成功,後面的佇列都要嘗試去獲取這個佇列的使用權,如果前面的佇列沒有及時釋放掉佇列使用權,其他佇列肯定會獲取失敗。

我們想要每個執行緒都能獲取到任意乙個佇列的使用權,又不想衝突率太高,可以用下面的方法。假設我現在有n個執行緒,k個佇列,假設n如果你的讀寫十分佔cpu,可能需要每個執行緒分配乙個核的時候,共享帶來的問題是不可忽視的。每個cpu都有各自的cache,具體跟cpu架構有關,但至少l1是每個核乙個的。為了保持cache同步,cpu採用了mesi協議去保證。簡單來說就是每個核監聽匯流排,可以知道哪些資料被修改過了,對於髒資料及時同步,同步要經過資料匯流排。如果每個執行緒繫結乙個核,佇列又是共享的,那cpu就要頻繁進行同步。同步是必不可少的,但是如果你只有乙個寫執行緒,又有多個讀執行緒的話,寫執行緒會被大大影響,從而降低了效能。這是因為讀執行緒所在的核需要和寫執行緒所在的核進行同步,匯流排容易被佔滿,寫自然就慢下來了。這種情況很難去優化,只能建議在綁核的時候,寫執行緒盡可能和讀綁在同個cpu上,跨cpu帶來的消耗更大。或者可以利用超執行緒技術,超執行緒上的兩個核是共享cache的,可以把執行緒兩兩繫結起來,但是這樣cpu算力會降低,因為超執行緒無法達到兩個核的算力。這種情況只能從降低同步量去解決了。

降低同步量的方法很多,盡可能使用執行緒私有的變數,而不是全域性變數,這樣不會帶來同步。或者是乙個執行緒連續處理多次同個佇列,降低移動的頻率,這樣同步數量也可以減少,但是會帶來上面講過的資料處理延遲。

false sharing 問題也是不可忽視的,cache同步的時候,兩個連續的記憶體分別在兩個核上,且在同個cache line。cpu每次更新資料的最小單位都是cache line。假設這兩個資料都要頻繁更新,那他們會不斷向對方發生更新資料請求,這樣開銷十分巨大(具體參考mesi協議,這裡不細說)。解決方法就是進行cache line對齊,每個變數都分配到不同cache line上就好。

mySQL無鎖佇列 go 無鎖佇列

無鎖佇列適用場景 兩個執行緒之間的互動資料,乙個執行緒生產資料,另外乙個執行緒消費資料,效率高 缺點 需要使用固定分配的空間,不能動態增加 減少長度,存在空間浪費和無法擴充套件空間問題 package main import fmt reflect strings time type loopque...

無鎖環形佇列

環形一讀一寫佇列中,不需要擔心unsigned long溢位問題,因為溢位後自動回歸,相減值還會保留。示例一 注 max count 必須為 2 的指數,即 2,4,8,16.佇列尺寸 define max count 4096 define max mask 4095 max count 1 變數...

KFIFO無鎖佇列

linux核心中實現了以非常漂亮的無鎖佇列,在只有乙個讀者和乙個寫者的情況下,無需上鎖,而擁有執行緒安全的特性,使得效能相比於加鎖方式實現的佇列提公升數倍 kfifo的分析可見 這位作者講的很清楚 kifio可以實現無鎖佇列,但是為什麼可以實現呢,通過這種方式為什麼可以執行緒安全?換句話說,平時實現...