https://blog.csdn.net/qq_41230604/article/details/126305068
C#線程安全隊列ConcurrentQueue
ConcurrentQueue隊列是一個高效的線程安全的隊列,是Net Framework 4.0,System.Collections.Concurrent命名空間下的一個數據結構。
ConcurrentQueue內部結構:
實現原理
眾所周知,在普通的非線程安全隊列有兩種實現方式:
1.使用數組實現隊列。 2.使用鏈表實現隊列。
看看兩種方式的優劣:
??.Net Farmework中的普通隊列Queue的實現使用了第一種方式,缺點是當隊列空間不足會進行擴容,擴容的主要實現是開辟一個原始長度2倍的新數組,然后將原始數組里面的數據復制到新數組中,所以當擴容時就會產生不小的內存開銷,在并發的環境中對性能的影響不可小視。當然在調用Queue的構造函數時可以指定默認空間的大小,但是一般情況下數據量是不可預測的,選大了會照成空間浪費,選小了會有復制內存的開銷,而且隊列擴容以后需要顯示調用TrimToSize()方法才能回收掉不使用的內存空間。
??第二種鏈表實現方式雖然消除了空間浪費的問題但是又增加了GC的壓力,當入隊時會分配一個新節點,出隊時要對該節點進行廢棄,對于大量的出隊入隊操作時該實現方式性能不高。
??綜合以上兩種實現方式,在支持多線程并發出隊并發入隊的情況下,ConcurrentQueue使用了分段存儲的概念(如上圖所示),ConcurrentQueue分配內存時以段(Segment)為單位,一個段內部含有一個默認長度為32的數組和執行下一個段的指針,有個和Head和Tail指針分別指向了起始段和結束段(這種結構有點像操作系統的段式內存管理和頁式內存管理策略)。這種分配內存的實現方式不但減輕的GC的壓力而且調用者也不用顯示的調用TrimToSize()方法回收內存(在某段內存為空時,會由GC來回收該段內存)。
Segment內部和用數組實現的普通隊列相當,只不過對于入隊和出隊操作使用了原子操作來防止多線程競爭問題,使用隨機退讓等技術保證活鎖等問題,實現機制和ConcurrentStack差別不大,跟多TryAppend的實現細節在源碼注釋中已經闡述的非常清楚這里就再做不過多的解釋。
主要成員函數
入隊(EnQueue) 、出隊(TryDequeue) 、是否為空(IsEmpty)、獲取隊列內元素數量(Count)。
void Enqueue(T item) 入隊函數
public void Enqueue(T item)
{
?? ?Spinwait spin = new Spinwait();
?? ?while (true)
?? ?{
?? ? ? ?Segment tail =m_tail;
?? ? ? ?if ( tail .TryAppend(item))
?? ? ? ?return;
?? ? ? ?spin.SpinOnce();
?? ?}
}
如上代碼所示,入隊操作是在尾部的段中進行,當數據進入段內失敗時會先進行一個回退操作然后再不斷嘗試直到成功,這里失敗的原因(tail.Append(item)返回false)只有一個就是當該段內的空間不夠時正在分配新的段,這段時間內會進入該段的元素會失敗。
當隊列已滿時會自動增加隊列容量。
bool TryDequeue(T result) 出隊函數
嘗試出隊函數,如果當前隊列為空,返回false,否則返回隊列的第一個元素。
public bool TryDequeue(out T result)
{
?? ?while (!IsEmpty)
?? ?{
?? ??? ?Segment head = m_head;
?? ??? ?if (head.TryRemove(out result))
?? ??? ??? ?return true;
?? ?}
?? ?result = default(T);
?? ?return false;
}
如上代碼所示,出隊失敗時返回false 而不是像入隊一樣進行回退操作,因為出隊失敗的原因只有一個就是當隊列內所有段的元素為空時,所以出隊設計成了返回bool值的函數。
bool TryPeek(T * result)
跟TryDequeue()方法相似,但不刪除隊列中的元素。
int Count()
返回當前隊列中元素的個數。
找到頭節點的low的位置和尾節點的high的位置,由于每個段內記錄了當前段在隊列中的索引,所以很容易求出整個隊列中元素的數量。
跟ConcurrentStack一樣 微軟官方文檔和注釋中也說明:判斷隊列是否為空要使用IsEmpty屬性而不是判斷Count == 0 原因在于GetHeadTailPositions在大量數據入隊和出隊的過程中尋找頭尾節點的位置是比較耗時的操作,要不斷循環確定頭尾節點的位置,所以判斷隊列是否為空還是使用IsEmpty屬性。
bool IsEmpty()
判定當前隊列為空。
整個判斷主要有三種情況:
1.頭節點(段)不為空返回false
2.頭節點為空而且下一個節點也為空返回true
3.頭節點為空而且下一個節點不為空返回false,這種情況說明隊列正在擴容,所以要自選等待擴容完畢時再次進行判斷
void Reset()
清空并復位隊列。