c#高效的執行緒安全佇列ConcurrentQueue<T>的實現
入隊(EnQueue) 、出隊(TryDequeue) 、是否為空(IsEmpty)、獲取佇列內元素數量(Count)。
一、ConcurrentQueue內部結構:
1.實現原理
眾所周知,在普通的非執行緒安全佇列有兩種實現方式:
1.使用陣列實現的迴圈佇列。
2.使用連結串列實現的佇列。
先看看兩種方式的優劣:
.Net Farmework中的普通佇列Queue的實現使用了第一種方式,缺點是當佇列空間不足會進行擴容,擴容的主要實現是開闢一個原始長度2倍的新陣列,然後將原始數組裡面的資料複製到新陣列中,所以當擴容時就會產生不小的記憶體開銷,在併發的環境中對效能的影響不可小視。當然在呼叫Queue的建構函式時可以指定預設空間的大小,但是一般情況下資料量是不可預測的,選大了會照成空間浪費,選小了會有複製記憶體的開銷,而且佇列擴容以後需要顯示呼叫TrimToSize()方法才能回收掉不使用的記憶體空間。
第二種連結串列實現方式雖然消除了空間浪費的問題但是又增加了GC的壓力,當入隊時會分配一個新節點,出隊時要對該節點進行廢棄,對於大量的出隊入隊操作時該實現方式效能不高。
綜合以上兩種實現方式,在支援多執行緒併發出隊併發入隊的情況下,ConcurrentQueue使用了分段儲存的概念(如上圖所示),ConcurrentQueue分配記憶體時以段(Segment)為單位,一個段內部含有一個預設長度為32的陣列和執行下一個段的指標,有個和Head和Tail指標分別指向了起始段和結束段(這種結構有點像作業系統的段式記憶體管理和頁式記憶體管理策略)。這種分配記憶體的實現方式不但減輕的GC的壓力而且呼叫者也不用顯示的呼叫TrimToSize()方法回收記憶體(在某段記憶體為空時,會由GC來回收該段記憶體)。
2.Segment(段)內部結構
其實對於ConcurrentQueue的操作其實就是對Segment(資料段)的操作。
Segment可抽象出如下資料結構:
Segment內部主要方法:
Segment內部和用陣列實現的普通佇列相當,只不過對於入隊和出隊操作使用了原子操作來防止多執行緒競爭問題,使用隨機退讓等技術保證活鎖等問題,實現機制和ConcurrentStack差別不大,跟多TryAppend的實現細節在原始碼註釋中已經闡述的非常清楚這裡就再做不過多的解釋。
二、入隊操作
如上圖所示,入隊操作是在尾部的段中進行,當資料進入段內失敗時會先進行一個回退操作然後再不斷嘗試直到成功,這裡失敗的原因(tail.Append(item)返回false)只有一個就是當該段內的空間不夠時正在分配新的段,這段時間內會進入該段的元素會失敗。
三、出隊操作
如上圖所示,出隊失敗時返回false 而不是像入隊一樣進行回退操作,因為出隊失敗的原因只有一個就是當佇列內所有段的元素為空時,所以出隊設計成了返回bool值的函式。
四、判斷是否為空(IsEmpty)
整個判斷為O(1)的複雜度 主要有三種情況:
1. 頭節點(段)不為空返回false
2. 頭節點為空而且下一個節點也為空返回true
3. 頭節點為空而且下一個節點不為空返回false,這種情況說明佇列正在擴容,所以要自選等待擴容完畢時再次進行判斷
五、獲取佇列內元素數量(Count)
找到頭節點的low的位置和尾節點的high的位置,由於每個段內記錄了當前段在佇列中的索引,所以很容易求出整個佇列中元素的數量。
跟ConcurrentStack一樣 微軟官方文件和註釋中也說明:判斷佇列是否為空要使用IsEmpty屬性而不是判斷Count == 0 原因在於GetHeadTailPositions在大量資料入隊和出隊的過程中尋找頭尾節點的位置是比較耗時的操作,要不斷迴圈確定頭尾節點的位置,所以判斷佇列是否為空還是使用IsEmpty屬性。
到此這篇關於c#高效的執行緒安全佇列ConcurrentQueue<T>的實現的文章就介紹到這了,更多相關c# ConcurrentQueue<T>內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!