什麼是執行緒安全?
答:執行緒安全是多執行緒程式設計時的計算機程式程式碼中的一個概念。在擁有共享資料的多條執行緒並行執行的程式中,執行緒安全的程式碼會通過同步機制保證各個執行緒都可以正常且正確的執行,不會出現資料汙染等意外情況。
前面幾篇寫的線性結構,在多執行緒並行的情況下會出現共享資料會執行緒間讀取與寫入不一直的情況,為了解決這種情況,通常會使用鎖來解決,也就是將並行改為序列。但是在使用穿行違背了使用多執行緒併發的初衷,這種情況下我們可以考慮採用執行緒安全結構。
先看下執行緒安全佇列的用法:
ConcurrentQueue<int> ts = new System.Collections.Concurrent.ConcurrentQueue<int>(); ts.Enqueue(1); ts.Enqueue(2); ts.Enqueue(3); ts.Enqueue(4); foreach (var r in ts) { Console.Write($"data:{r} "); } Console.WriteLine(); ts.TryPeek(out int pk); Console.WriteLine($"peek:{pk}"); ts.TryDequeue(out int ck); ts.Enqueue(5); ts.Enqueue(6); Console.WriteLine(); foreach (var r in ts) { Console.Write($"data:{r} "); } Console.WriteLine(); Console.ReadLine();
現在我們看下執行緒安全佇列的實現方式:(參考自:.net framework 4.8),核心程式碼全部做了註釋。
總的來說,(總結語放到前面,防止程式碼篇幅太大,同志們沒有耐心翻到最底下~)
1、執行緒安全佇列通過SpinWait自旋類來實現等待並行執行緒完成與Interlocked原子操作類計數實現的。
2、執行緒安全佇列通過單向連結串列實現的,鏈的節點為長度32的陣列,通過記錄鏈的頭節點與尾節點、以及佇列的頭尾實現佇列的儲存與入隊、出隊操作的。
public class MyConcurrentQueue<T> : IProducerConsumerCollection<T> { [NonSerialized] private volatile Segment m_head; [NonSerialized] private volatile Segment m_tail; private T[] m_serializationArray; private const int SEGMENT_SIZE = 32; [NonSerialized] internal volatile int m_numSnapshotTakers = 0; /// <summary> /// 鏈尾部節點 /// </summary> public MyConcurrentQueue() { m_head = m_tail = new Segment(0, this); } //嘗試新增 bool IProducerConsumerCollection<T>.TryAdd(T item) { Enqueue(item); return true; } /// <summary> /// 嘗試從中移除並返回物件 /// </summary> /// <param name="item"> /// </remarks> bool IProducerConsumerCollection<T>.TryTake(out T item) { return TryDequeue(out item); } /// <summary> /// 判斷當前鏈是否為空 /// </summary> public bool IsEmpty { get { Segment head = m_head; if (!head.IsEmpty) //如果頭不為空,則鏈非空 return false; else if (head.Next == null) //如果頭節點的下一個節點為空,且為鏈尾, return true; else //如果頭節點為空且不是最後一個節點 ,則標識另一個執行緒正在寫入該陣列 //等待中.. { SpinWait spin = new SpinWait(); while (head.IsEmpty) { //此時為空 if (head.Next == null) return true; //否則標識正在有執行緒佔用寫入 //執行緒迴圈一次 spin.SpinOnce(); head = m_head; } return false; } } } /// <summary> /// 用來判斷鏈是否在變化 /// </summary> /// <param name="head"></param> /// <param name="tail"></param> /// <param name="headLow"></param> /// <param name="tailHigh"></param> private void GetHeadTailPositions(out Segment head, out Segment tail, out int headLow, out int tailHigh) { head = m_head; tail = m_tail; headLow = head.Low; tailHigh = tail.High; SpinWait spin = new SpinWait(); Console.WriteLine($"head.Low:{head.Low},tail.High:{tail.High},head.m_index:{head.m_index},tail.m_index:{tail.m_index}"); //通過迴圈來保證值不再更改(也就是說並行執行緒操作結束) //保證執行緒序列核心的判斷邏輯 while ( //頭尾發生變化 head != m_head || tail != m_tail //如果佇列頭、尾索引發生變化 || headLow != head.Low || tailHigh != tail.High || head.m_index > tail.m_index) { spin.SpinOnce(); head = m_head; tail = m_tail; headLow = head.Low; tailHigh = tail.High; } } /// <summary> /// 獲取總數 /// </summary> public int Count { get { Segment head, tail; int headLow, tailHigh; GetHeadTailPositions(out head, out tail, out headLow, out tailHigh); if (head == tail) { return tailHigh - headLow + 1; } //頭節點長度 int count = SEGMENT_SIZE - headLow; //加上中間其他節點長度 count += SEGMENT_SIZE * ((int)(tail.m_index - head.m_index - 1)); //加上尾節點長度 count += tailHigh + 1; return count; } } public object SyncRoot => throw new NotImplementedException(); public bool IsSynchronized => throw new NotImplementedException(); public void CopyTo(T[] array, int index) { } /// <summary> /// 暫未實現 /// </summary> /// <returns></returns> public IEnumerator<T> GetEnumerator() { return null; } /// <summary> /// 新增 /// </summary> /// <param name="item"></param> public void Enqueue(T item) { SpinWait spin = new SpinWait(); while (true) { Segment tail = m_tail; if (tail.TryAppend(item)) return; spin.SpinOnce(); } } /// <summary> /// 嘗試刪除節點 /// </summary> /// <param name="result"></param> /// <returns></returns> public bool TryDequeue(out T result) { while (!IsEmpty) { Segment head = m_head; if (head.TryRemove(out result)) return true; } result = default(T); return false; } /// <summary> /// 檢視最後一個新增入的元素 /// </summary> /// <param name="result"></param> /// <returns></returns> public bool TryPeek(out T result) { //原子增加值 Interlocked.Increment(ref m_numSnapshotTakers); while (!IsEmpty) { //首先從頭節點看一下第一個節點是否存在 Segment head = m_head; if (head.TryPeek(out result)) { Interlocked.Decrement(ref m_numSnapshotTakers); return true; } } result = default(T); Interlocked.Decrement(ref m_numSnapshotTakers); return false; } public void CopyTo(Array array, int index) { throw new NotImplementedException(); } IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } public T[] ToArray() { throw new NotImplementedException(); } /// <summary> /// 為執行緒安全佇列提供一個 單向連結串列, /// 連結串列的每個節點儲存長度為32的陣列 /// </summary> private class Segment { /// <summary> /// 定義一個陣列,用於儲存每個節點的內容 /// </summary> internal volatile T[] m_array; /// <summary> /// 定義一個結構陣列,用於標識陣列中每個節點是否有效(是否儲存內容) /// </summary> internal volatile VolatileBool[] m_state; //指標,指向下一個節點陣列 //如果是最後一個節點,則節點為空 private volatile Segment m_next; /// <summary> /// 索引,用來儲存連結串列的長度 /// </summary> internal readonly long m_index; /// <summary> /// 用來標識佇列頭-陣列彈出索引 /// </summary> private volatile int m_low; /// <summary> /// 用來標識佇列尾-陣列最新儲存位置 /// </summary> private volatile int m_high; /// <summary> /// 用來標識佇列 /// </summary> private volatile MyConcurrentQueue<T> m_source; /// <summary> /// 例項化鏈節點 /// </summary> internal Segment(long index, MyConcurrentQueue<T> source) { m_array = new T[SEGMENT_SIZE]; m_state = new VolatileBool[SEGMENT_SIZE]; //all initialized to false m_high = -1; m_index = index; m_source = source; } /// <summary> /// 連結串列的下一個節點 /// </summary> internal Segment Next { get { return m_next; } } /// <summary> /// 如果當前節點陣列為空返回true, /// </summary> internal bool IsEmpty { get { return (Low > High); } } /// <summary> /// 非安全新增方法(無判斷陣列長度) /// </summary> /// <param name="value"></param> internal void UnsafeAdd(T value) { m_high++; m_array[m_high] = value; m_state[m_high].m_value = true; } internal Segment UnsafeGrow() { Segment newSegment = new Segment(m_index + 1, m_source); m_next = newSegment; return newSegment; } /// <summary> /// 如果當前陣列滿了 >=32,則鏈擴充套件節點。 /// </summary> internal void Grow() { //重新船艦陣列 Segment newSegment = new Segment(m_index + 1, m_source); //賦值給next指標 m_next = newSegment; //將節點新增到鏈 m_source.m_tail = m_next; } /// <summary> /// 在末尾新增元素 /// </summary> /// <param name="value">元素</param> /// <param name="tail">The tail.</param> /// <returns>如果附加元素,則為true;如果當前陣列已滿,則為false</returns> /// <remarks>如果附加指定的元素成功,並且在此之後陣列滿了,在鏈上新增新節點(節點為32長度陣列) </remarks> internal bool TryAppend(T value) { //如果陣列已滿則跳出方法 if (m_high >= SEGMENT_SIZE - 1) { return false; } //區域性變數初始化 int newhigh = SEGMENT_SIZE; try { } finally { //原子遞增 newhigh = Interlocked.Increment(ref m_high); if (newhigh <= SEGMENT_SIZE - 1) { m_array[newhigh] = value; m_state[newhigh].m_value = true; } //如果陣列滿了,則擴充套件鏈節點。 if (newhigh == SEGMENT_SIZE - 1) { Grow(); } } //如果 newhigh <= SEGMENT_SIZE-1, 這意味著當前執行緒成功地佔據了一個位置 return newhigh <= SEGMENT_SIZE - 1; } /// <summary> /// 嘗試從鏈的頭部陣列刪除節點 /// </summary> /// <param name="result"></param> /// <returns></returns> internal bool TryRemove(out T result) { SpinWait spin = new SpinWait(); int lowLocal = Low, highLocal = High; while (lowLocal <= highLocal) { //獲取隊頭索引 if (Interlocked.CompareExchange(ref m_low, lowLocal + 1, lowLocal) == lowLocal) { //如果要彈出佇列的值不可用,說明這個位置被並行執行緒獲取到了許可權,但是值還未寫入。 //通過執行緒自旋等待值寫入 SpinWait spinLocal = new SpinWait(); while (!m_state[lowLocal].m_value) { spinLocal.SpinOnce(); } //取出值 result = m_array[lowLocal]; // 如果沒有其他執行緒讀取(GetEnumerator()、ToList()) 執行刪除 // 如 TryPeek 的時候m_numSnapshotTakers會在進入方法體時++,在出方法體-- // 清空該索引下的值 if (m_source.m_numSnapshotTakers <= 0) m_array[lowLocal] = default(T); //如果說lowLocal+1 = 32 說明當前鏈節點的陣列已經全部出隊 if (lowLocal + 1 >= SEGMENT_SIZE) { //由於lowLocal <= highLocal成立 //lowLocal + 1 >= SEGMENT_SIZE 如果成立 ,且m_next == null 成立, //說明在此時有其他執行緒正在做擴充套件鏈結構 //那麼當前執行緒需要等待其他執行緒完成擴充套件連結串列,再做出隊操作。 spinLocal = new SpinWait(); while (m_next == null) { spinLocal.SpinOnce(); } m_source.m_head = m_next; } return true; } else { //此時說明 當前執行緒競爭資源失敗,做短暫自旋後繼續競爭資源 spin.SpinOnce(); lowLocal = Low; highLocal = High; } } //失敗的情況下返回空值 result = default(T); return false; } /// <summary> /// 嘗試獲取佇列頭節點元素 /// </summary> internal bool TryPeek(out T result) { result = default(T); int lowLocal = Low; //校驗當前佇列是否正確 if (lowLocal > High) return false; SpinWait spin = new SpinWait(); //如果頭節點無效,則說明當前節點被其他執行緒佔用,並在做寫入操作, //需要等待其他執行緒寫入後再執行讀取操作 while (!m_state[lowLocal].m_value) { spin.SpinOnce(); } result = m_array[lowLocal]; return true; } /// <summary> /// 返回佇列首位置 /// </summary> internal int Low { get { return Math.Min(m_low, SEGMENT_SIZE); } } /// <summary> /// 獲取佇列長度 /// </summary> internal int High { get { //如果m_high>SEGMENT_SIZE,則表示超出範圍,我們應該返回 SEGMENT_SIZE-1 return Math.Min(m_high, SEGMENT_SIZE - 1); } } } }
/// <summary> /// 結構-用來儲存整陣列每個索引上是否儲存值 /// </summary> struct VolatileBool { public VolatileBool(bool value) { m_value = value; } public volatile bool m_value; }
程式碼通篇看下來有些長(已經精簡了很多,只實現入隊、出隊、與檢視下一個出隊的值),不知道有多少人能翻到這裡~
說明:
1、TryAppend方法通過Interlocked.Increment()原子遞增方法獲取下一個陣列儲存點,通過比對32判斷鏈是否需要增加下一個鏈節點,也就是說,鏈的儲存空間每次擴充套件為32個儲存位置。
2、TryRemove方法通過 Interlocked.CompareExchange()方法來判斷當前是否有並行執行緒在寫入,如果有則通過 while迴圈 SpinWait類的SpinOnce()方法實現等待寫入完成後,再做刪除;特別說明,判斷是否寫入是靠VolatileBool結構來實現的,每個連結串列的每個節點在儲存值的同時每個儲存都對應一個VolatileBool結構用來標識當前寫入點是否成功寫入。特殊情況,如果當前鏈節點的陣列已經空了,則需要pinWait類的SpinOnce()簡短的自旋等待並行的寫入方法完成擴充套件鏈後,再做刪除。
3、TryPeek方法,同樣會判斷要獲取的元素是否已經成功寫入(不成功則說明並行執行緒還未完成寫入),如果未完成,則通過 while pinWait類的SpinOnce()來等待寫入完成後,再讀取元素內容。
現在程式碼已經看完了,來試下:
MyConcurrentQueue<string> myConcurrentQueue = new MyConcurrentQueue<string>(); for (int i = 0; i < 67; i++) { myConcurrentQueue.Enqueue($"第{i}位"); Console.WriteLine($"總數:{myConcurrentQueue.Count}"); } myConcurrentQueue.TryPeek(out string rs); Console.WriteLine($"TryPeek 總數:{myConcurrentQueue.Count}"); for (int i = 0; i < 34; i++) { myConcurrentQueue.TryDequeue(out string result0); Console.WriteLine($"TryDequeue 總數:{myConcurrentQueue.Count}"); } Console.ReadKey();
列印:
head.Low:0,tail.High:0,head.m_index:0,tail.m_index:0 總數:1 head.Low:0,tail.High:1,head.m_index:0,tail.m_index:0 總數:2 head.Low:0,tail.High:2,head.m_index:0,tail.m_index:0 總數:3 head.Low:0,tail.High:3,head.m_index:0,tail.m_index:0 總數:4 head.Low:0,tail.High:4,head.m_index:0,tail.m_index:0 總數:5 head.Low:0,tail.High:5,head.m_index:0,tail.m_index:0 總數:6 head.Low:0,tail.High:6,head.m_index:0,tail.m_index:0 總數:7 head.Low:0,tail.High:7,head.m_index:0,tail.m_index:0 總數:8 head.Low:0,tail.High:8,head.m_index:0,tail.m_index:0 總數:9 head.Low:0,tail.High:9,head.m_index:0,tail.m_index:0 總數:10 head.Low:0,tail.High:10,head.m_index:0,tail.m_index:0 總數:11 head.Low:0,tail.High:11,head.m_index:0,tail.m_index:0 總數:12 head.Low:0,tail.High:12,head.m_index:0,tail.m_index:0 總數:13 head.Low:0,tail.High:13,head.m_index:0,tail.m_index:0 總數:14 head.Low:0,tail.High:14,head.m_index:0,tail.m_index:0 總數:15 head.Low:0,tail.High:15,head.m_index:0,tail.m_index:0 總數:16 head.Low:0,tail.High:16,head.m_index:0,tail.m_index:0 總數:17 head.Low:0,tail.High:17,head.m_index:0,tail.m_index:0 總數:18 head.Low:0,tail.High:18,head.m_index:0,tail.m_index:0 總數:19 head.Low:0,tail.High:19,head.m_index:0,tail.m_index:0 總數:20 head.Low:0,tail.High:20,head.m_index:0,tail.m_index:0 總數:21 head.Low:0,tail.High:21,head.m_index:0,tail.m_index:0 總數:22 head.Low:0,tail.High:22,head.m_index:0,tail.m_index:0 總數:23 head.Low:0,tail.High:23,head.m_index:0,tail.m_index:0 總數:24 head.Low:0,tail.High:24,head.m_index:0,tail.m_index:0 總數:25 head.Low:0,tail.High:25,head.m_index:0,tail.m_index:0 總數:26 head.Low:0,tail.High:26,head.m_index:0,tail.m_index:0 總數:27 head.Low:0,tail.High:27,head.m_index:0,tail.m_index:0 總數:28 head.Low:0,tail.High:28,head.m_index:0,tail.m_index:0 總數:29 head.Low:0,tail.High:29,head.m_index:0,tail.m_index:0 總數:30 head.Low:0,tail.High:30,head.m_index:0,tail.m_index:0 總數:31 head.Low:0,tail.High:-1,head.m_index:0,tail.m_index:1 總數:32 head.Low:0,tail.High:0,head.m_index:0,tail.m_index:1 總數:33 head.Low:0,tail.High:1,head.m_index:0,tail.m_index:1 總數:34 head.Low:0,tail.High:2,head.m_index:0,tail.m_index:1 總數:35 head.Low:0,tail.High:3,head.m_index:0,tail.m_index:1 總數:36 head.Low:0,tail.High:4,head.m_index:0,tail.m_index:1 總數:37 head.Low:0,tail.High:5,head.m_index:0,tail.m_index:1 總數:38 head.Low:0,tail.High:6,head.m_index:0,tail.m_index:1 總數:39 head.Low:0,tail.High:7,head.m_index:0,tail.m_index:1 總數:40 head.Low:0,tail.High:8,head.m_index:0,tail.m_index:1 總數:41 head.Low:0,tail.High:9,head.m_index:0,tail.m_index:1 總數:42 head.Low:0,tail.High:10,head.m_index:0,tail.m_index:1 總數:43 head.Low:0,tail.High:11,head.m_index:0,tail.m_index:1 總數:44 head.Low:0,tail.High:12,head.m_index:0,tail.m_index:1 總數:45 head.Low:0,tail.High:13,head.m_index:0,tail.m_index:1 總數:46 head.Low:0,tail.High:14,head.m_index:0,tail.m_index:1 總數:47 head.Low:0,tail.High:15,head.m_index:0,tail.m_index:1 總數:48 head.Low:0,tail.High:16,head.m_index:0,tail.m_index:1 總數:49 head.Low:0,tail.High:17,head.m_index:0,tail.m_index:1 總數:50 head.Low:0,tail.High:18,head.m_index:0,tail.m_index:1 總數:51 head.Low:0,tail.High:19,head.m_index:0,tail.m_index:1 總數:52 head.Low:0,tail.High:20,head.m_index:0,tail.m_index:1 總數:53 head.Low:0,tail.High:21,head.m_index:0,tail.m_index:1 總數:54 head.Low:0,tail.High:22,head.m_index:0,tail.m_index:1 總數:55 head.Low:0,tail.High:23,head.m_index:0,tail.m_index:1 總數:56 head.Low:0,tail.High:24,head.m_index:0,tail.m_index:1 總數:57 head.Low:0,tail.High:25,head.m_index:0,tail.m_index:1 總數:58 head.Low:0,tail.High:26,head.m_index:0,tail.m_index:1 總數:59 head.Low:0,tail.High:27,head.m_index:0,tail.m_index:1 總數:60 head.Low:0,tail.High:28,head.m_index:0,tail.m_index:1 總數:61 head.Low:0,tail.High:29,head.m_index:0,tail.m_index:1 總數:62 head.Low:0,tail.High:30,head.m_index:0,tail.m_index:1 總數:63 head.Low:0,tail.High:-1,head.m_index:0,tail.m_index:2 總數:64 head.Low:0,tail.High:0,head.m_index:0,tail.m_index:2 總數:65 head.Low:0,tail.High:1,head.m_index:0,tail.m_index:2 總數:66 head.Low:0,tail.High:2,head.m_index:0,tail.m_index:2 總數:67 head.Low:0,tail.High:2,head.m_index:0,tail.m_index:2 TryPeek 總數:67 head.Low:1,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:66 head.Low:2,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:65 head.Low:3,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:64 head.Low:4,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:63 head.Low:5,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:62 head.Low:6,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:61 head.Low:7,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:60 head.Low:8,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:59 head.Low:9,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:58 head.Low:10,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:57 head.Low:11,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:56 head.Low:12,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:55 head.Low:13,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:54 head.Low:14,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:53 head.Low:15,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:52 head.Low:16,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:51 head.Low:17,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:50 head.Low:18,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:49 head.Low:19,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:48 head.Low:20,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:47 head.Low:21,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:46 head.Low:22,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:45 head.Low:23,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:44 head.Low:24,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:43 head.Low:25,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:42 head.Low:26,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:41 head.Low:27,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:40 head.Low:28,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:39 head.Low:29,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:38 head.Low:30,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:37 head.Low:31,tail.High:2,head.m_index:0,tail.m_index:2 TryDequeue 總數:36 head.Low:0,tail.High:2,head.m_index:1,tail.m_index:2 TryDequeue 總數:35 head.Low:1,tail.High:2,head.m_index:1,tail.m_index:2 TryDequeue 總數:34 head.Low:2,tail.High:2,head.m_index:1,tail.m_index:2 TryDequeue 總數:33
有時間希望大家能將程式碼跑一下,相信會更明白其中的原理。