多執行緒-2(執行緒同步)
帶著問題去思考!大家好。
今天我們來了解下什麼是執行緒同步?
首先我們先知道這些概念和一些類;
- 執行基本的原子性
- Mutex類
- SemaphoreSlim類
- AutoResetEvent類
- ManualRestEventSlim類
- CountDownEvent類
- Barrier類
- ReaderWriterLockSilm類
- SpinWait類
我們都知道確保當一個執行緒使用某些資源的時候,同時其他執行緒無法使用該資源。這引入一個概念是共享資源。
多個執行緒同時使用共享物件會造成很多問題。同步執行緒使得對共享物件的操作能夠以正確的順序執行是非常重要的。
首先通過一個加減例子瞭解下lock處理
public abstract class CounterBase { public abstract void Increment(); public abstract void Decrement(); } public class Counter:CounterBase { public int Count { get;private set; } public override void Decrement() { Count++; } public override void Increment() { Count--; } } public class CounterLock : CounterBase { private readonly object _synclock = new object(); public int Count { get; private set; } public override void Decrement() { lock(_synclock) { Count++; } } public override void Increment() { lock (_synclock) { Count--; } } } static void Main(string[] args) { #region 多執行緒鎖 Console.WriteLine("Incorrect counter"); var c = new Counter(); var t1 = new Thread(() => TestCount(c)); var t2= new Thread(() => TestCount(c)); var t3 = new Thread(() => TestCount(c)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); Console.WriteLine("Total count:{0}",c.Count); Console.WriteLine("-----------"); Console.WriteLine("Correct counter"); var c1 = new CounterLock(); t1 = new Thread(() => TestCount(c1)); t2 = new Thread(() => TestCount(c1)); t3 = new Thread(() => TestCount(c1)); t1.Start(); t2.Start(); t3.Start(); t1.Join(); t2.Join(); t3.Join(); Console.WriteLine("Total count:{0}", c1.Count); #endregion } static void TestCount(CounterBase c) { for (int i = 0; i < 1000; i++) { c.Increment(); c.Decrement(); } }
我們知道,最終結果應該是0;
因為第一個執行緒得到count為10增加為11,第二個執行緒得到的值是11並增加為12.第一個執行緒得到count為12,但是遞減操作發生前,第二個執行緒得到的值也是12,。然後第一個執行緒將12遞減11並儲存count中,同時第二個執行緒進行了同樣的操作,結果我們進行了兩次遞增操作但是隻有一次遞減操作。這是競爭條件(race condition)
1:請儘量避免使用共享物件
2:必須是共享的狀態時候,使用原子操作。一個操作只佔一個量子的時間,一次完成。這說明只有當前操作完成後,其他執行緒才能執行其他操作,避免死鎖,和使用鎖
3:使用不同方式來協調執行緒
- 將等待的執行緒置於阻塞狀態。當執行緒阻塞狀態,只會佔用少量CPU時間,意味將引入至少一次所謂的上下文切換(context switch上下文切換是作業系統的執行緒排程器).該排程器會儲存等待的執行緒等待,並切換到另一個執行緒,依次恢復等待的執行緒的狀態。這需要消耗很多資源,但是如果執行緒要被掛起很長時間的話,這是值得的---核心模式(kernel-mode)
- 執行緒只需要等待一小段時間,不用將執行緒切換到阻塞狀態。雖然執行緒等待時會浪費CPU時間,但節省了上下文切換耗費的CPU時間----使用者模式(user-mode)
- 先嚐試使用使用者模式等待,如果執行緒等待足夠長時間。則會切換到阻塞狀態節省CPU資源---混合模式(hybrid)
執行基本的原子操作
不用阻塞執行緒就可避免競爭條件
public class CounterLock : CounterBase { public int Count { get; private set; } public int _count; public override void Decrement() { Interlocked.Decrement(ref _count); } public override void Increment() { Interlocked.Increment(ref _count); } }
我們修改CounterLock,再來看看結果
我們可能會得到0,但是最終會得到一些不確定的非0.第一個例子是執行緒不安全的。第二個例子中,我們藉助Interlocked類,無需鎖定任何物件即可獲取正確結果。Interlocked提供了Increment.Decrement和Add基於數學操作的原子方法,編寫Counter類時無需使用鎖、
Mutex類
const string MutexName = "CSharpThreadingCookbook"; static void Main(string[] args) { using (var m=new Mutex(false,MutexName)) { if(!m.WaitOne(TimeSpan.FromSeconds(5),false)) { Console.WriteLine("Second instance is running!"); } else { Console.WriteLine("Running!"); Console.ReadLine(); m.ReleaseMutex(); } } }
程式啟動,定義一個指定名稱的互斥量,設定initialOwner標誌為false,這意味著如果互斥量已經被建立,則允許程式獲取該互斥量。如果沒有獲得互斥量,程式則簡單顯示Running
在運行同樣一個程式,則會在5秒種內嘗試獲得互斥量,如果此時在第一個程式中按下任意鍵,第二個程式則會開始執行。然而如果保持等待5秒,第二個程式無法獲得該互斥量
&n