上一章講了基元線程同步構造,而其它的線程同步構造都是基於這些基元線程同步構造的,並且一般都合併了用戶模式和內核模式構造,我們稱之為混合線程同步構造。 在沒有線程競爭時,混合線程提供了基於用戶模式構造所具備的性能優勢,而多個線程競爭一個構造時,混合線程通過基元內核模式的構造來提供不“自旋”的優勢。 那 ...
上一章講了基元線程同步構造,而其它的線程同步構造都是基於這些基元線程同步構造的,並且一般都合併了用戶模式和內核模式構造,我們稱之為混合線程同步構造。
在沒有線程競爭時,混合線程提供了基於用戶模式構造所具備的性能優勢,而多個線程競爭一個構造時,混合線程通過基元內核模式的構造來提供不“自旋”的優勢。
那麼接下來就是個簡單的混合線程同步構造的例子,可與上一章最後的那些例子相比較:
public class SimpleHybridLock : IDisposable { private Int32 m_waiters = 0; private AutoResetEvent m_waiterlock = new AutoResetEvent(false);//註意這裡是false public void Enter() { if (Interlocked.Increment(ref m_waiters)==1) { return; } m_waiterlock.WaitOne(); } public void Leave() { if (Interlocked.Decrement(ref m_waiters) == 0) { return; } m_waiterlock.Set(); } public void Dispose() { m_waiterlock.Dispose(); } }
上面的例子學了上一張後看起來感覺很簡單就不講解了,只是一個簡單的,將Interlocked這種互鎖構造和自動重置事件構造AutoResetEvent 結合起來的,混合線程同步構造的例子。
上面混合鎖可以去加入自旋,當超過一定的自旋次數時再進行阻塞。也可以去加入互斥體的遞歸玩法,總之這個東西充滿了無限的可能。
.NET 框架類庫中的混合構造
總體而言,實際上就是對上面那個簡單例子的擴展,它們的目的都是為了使線程能儘可能不去進入內核模式,並且減少線程競爭時自旋的性能影響。
- ManualResetEventSlim類和SemaphoreSlim類
- 翻譯過來就是手工重置事件簡化構造和信號量簡化構造
- 發生第一次競爭時才進行內核模式構造,否則為用戶模式構造
- 可傳遞超時值和CancellationToken,也就是取消啦,信號量那個還能進行非同步等待。
- Monitor類和同步塊
- Monitor類是最常用的,支持遞歸,線程所有權和互斥
- 然而這個類存在一些問題,容易引發BUG。因為它是一個靜態類,它的正確玩法在一定程度上和其它同步構造有所區別。
- 堆中的每個對象都可以關聯一個叫同步塊的數據結構,它為內核對象,且擁有線程ID,遞歸計數,等待線程計數。而Monitor類的操作就涉及到這些同步塊的欄位。
- 每個對象都有一個同步塊索引,而同步塊實際上是在CLR初始化的時候就創建的一個同步塊數組中。
- 一個對象在構造時它的同步塊索引為-1,就是沒有關聯任何同步塊。而調用Monitor.Enter後CLR在同步塊數組中找到個空白同步塊,並設置對象的同步塊索引,讓它引用該同步塊。Exit當然就是取消關聯。
- Monitor.Enter會傳一個對象進去,這個對象必須為所在函數的類的私有對象,而不能傳所在對象本身,這回讓這個鎖變成公共的。這樣就會引發很多問題。所以最好的方法就是傳遞一個私有的只讀對象。
- 永遠不要講String,值類型和類型對象傳給Monitor.Enter。
- 而C#有一個lock關鍵字提供的簡化語法就是基於Monitor的。而且其相當於在一個try finally結構上使用。首先不利於性能,其次還可能造成線程訪問損壞的狀態。所以作者建議杜絕使用lock語法。
- LockToken變數預設false,只有在Enter調用後才為true,要是在Enter調用前Exit,可以考慮判斷LockToken,從而避免錯誤的Exit。
- ReaderWriterLockSlim類
- 它的特點:
- 一個線程向數據寫入時,請求訪問的其他所有線程都被阻塞
- 一個線程從數據讀取時,請求讀取的其它線程允許繼續執行,但請求寫入的線程仍被阻塞。
- 向線程寫入的線程結束後,要麼解除一個寫入線程的阻塞,使它能向數據寫入,要麼解除所有讀取線程的阻塞,使它們能併發讀取數據。如果沒有線程被阻塞,鎖就進入可以自由使用的狀態,可供下一個reader或writer線程獲取。
- 從數據讀取所有線程結束後,一個writer線程被解除阻塞,使它能向數據寫入。如果沒有線程被阻塞,鎖就進入可以自由使用的狀態,可供下一個reader或writer線程獲取。
- 根據以上特點有EnterReadLock和EnterWriteLock兩種玩法,兩種玩法跟之前的那些例子都類似,只是效果不同,這裡就不舉例了。
- 它的特點:
雖然提供了這麼多同步構造,且玩法也很多。但是最重要的還是一點:能儘量避免就避免阻塞線程,否則應儘量使用Volatile和Interlocked方法,因為它們速度快,然而這兩個只能操作簡單類型。
一定要阻塞,就可以使用Monitor類,也可以用ReaderWriterLockSlim類,雖然比Monitor慢,但是允許多個線程併發進行,提升了總體性能,減少阻塞線程的幾率。
用System.Lazy類或者System.Threading.LazyInitializer類去替代雙檢索玩法。
一句話解決這個點:
Lazy<String> s=new Lazy<String>(()=>DateTime.Now.ToLongTimeString(),true);
調用的話就用s.Value,實際上就是封裝了雙檢索,有些地方加了些優化。目的就是延時載入。
非同步鎖
其實叫非同步的同步構造,因為一般的同步構造都是用阻塞線程或者自旋來完成,而非同步鎖的目的就是為了不阻塞來玩。
SemaphoreSlim類的WaitAsync方法就是這個思路,信號量玩法而已。
而reader-writer語義的玩法是ConcurrentExclusiveSchedulerPair類。(當沒有ConcurrentScheduler任務時,使用ExclusiveScheduler為獨占式運行。沒有ExclusiveScheduler運行時,ConcurrentScheduler調度的任務可同時進行)
併發集合類
FCL自帶四個線程安全的集合類,全在System.Collections.Concurrent(Concurrent為併發的意思)命名空間中定義。
它們是ConcurrentQueue,ConcurrentStack,ComcurrentDictionary和ConcurrentBag。
所有這些都是“非阻塞“的。(實際上在ConcurrentQueue,ConcurrentStack和ConcurrentBag為空的時候還要提取數據,那麼提取數據的這個線程就會被阻塞)