前言 當線程池的線程阻塞時,線程池會創建額外的線程,而創建、銷毀和調度線程所需要相當昂貴的記憶體資源,另外,很多的開發人員看見自己程式的線程沒有做任何有用的事情時習慣創建更多的線程,為了構建可伸縮、響應靈敏的程式,我們在前面介紹了線程也瘋狂 非同步編程。 但是非同步編程同樣也存在著很嚴重的問題,如果兩個不 ...
前言
當線程池的線程阻塞時,線程池會創建額外的線程,而創建、銷毀和調度線程所需要相當昂貴的記憶體資源,另外,很多的開發人員看見自己程式的線程沒有做任何有用的事情時習慣創建更多的線程,為了構建可伸縮、響應靈敏的程式,我們在前面介紹了線程也瘋狂-----非同步編程。
但是非同步編程同樣也存在著很嚴重的問題,如果兩個不同的線程訪問相同的變數和數據,按照我們非同步函數的實現方式,不可能存在兩個線程同時訪問相同的數據,這個時候我們就需要線程同步。多個線程同時訪問共用數據的時,線程同步能防止數據損壞,之所以強調同時這個概念,因為線程同步本質就是計時問題。
非同步和同步是相對的,同步就是順序執行,執行完一個再執行下一個,需要等待、協調運行。非同步就是彼此獨立,在等待某事件的過程中繼續做自己的事,不需要等待這一事件完成後再工作。線程就是實現非同步的一個方式。非同步是讓調用方法的主線程不需要同步等待另一線程的完成,從而可以讓主線程乾其它的事情。
基元用戶模式和內核模式構造
基礎概念
基元:可以在代碼中使用的簡單的構造
用戶模式:通過特殊的CPU指令協調線程,操作系統永遠檢測不到一個線程在基元用戶模式的構造上阻塞。
內核模式:由windows自身提供,在應用程式的線程中調用由內核實現的函數。
用戶模式構造
易變構造
C#編譯器、JIT編譯器和CPU都會對代碼進行優化,它們儘量保證保留我們的意圖,但是從多線程的角度出發,我們的意圖並不一定會得到保留,下麵舉例說明:
1 static void Main(string[] args) 2 { 3 Console.WriteLine("讓worker函數運行5s後停止"); 4 var t = new Thread(Worker); 5 t.Start(); 6 Thread.Sleep(5000); 7 stop = true; 8 9 Console.ReadLine(); 10 } 11 12 private static bool stop = false; 13 14 private static void Worker(object obj) 15 { 16 int x = 0; 17 while (!stop) 18 { 19 x++; 20 } 21 Console.WriteLine("worker函數停止x={0}",x); 22 }
編譯器如果檢查到stop為false,就生成代碼來進入一個無限迴圈,併在迴圈中一直遞增x,所以優化迴圈很快完成,但是編譯器只檢測stop一次,並不是每次都會檢測。
例子2---兩個線程同時訪問:
1 class test 2 { 3 private static int m_flag = 0; 4 5 private static int m_value = 0; 6 7 public static void Thread1(object obj) 8 { 9 m_value = 5; 10 m_flag = 1; 11 12 } 13 14 public static void Thread2(object obj) 15 { 16 if (m_flag == 1) 17 Console.WriteLine("m_value = {0}", m_value); 18 } 19 20 //多核CPU機器才會出現線程同步問題 21 public void Exec() 22 { 23 var thread1 = new Thread(Thread1); 24 var thread2 = new Thread(Thread2); 25 thread1.Start(); 26 thread2.Start(); 27 Console.ReadLine(); 28 } 29 }
程式在執行的時候,編譯器必須將變數m_flag和m_value從RAM讀入CPU寄存器,RAM先傳遞m_value的值0,thread1把值變為5,但是thread2並不知道thread2仍然認為值為0,這種問題一般來說發生在多核CPU的概率大一些,應該CPU越多,多個線程同時訪問資源的幾率就越大。
關鍵字volatile,作用禁止C#編譯器、JTP編譯器和CPU執行的一些優化,如果做用於變數後,將不允許欄位緩存到CPU的寄存器中,確保欄位的讀寫都在RAM中進行。
互鎖構造
System.Threading.Interlocked類中的每個方法都執行一次原子的讀取以及寫入操作,調用某個Interlocked方法之前的任何變數寫入都在這個Interlocked方法調用之前執行,而調用之後的任何變數讀取都在這個調用之後讀取。
Interlocked方法主要是對INT32變數進行靜態操作Add、Decrement、Compare、Exchange、CompareChange等方法,也接受object、Double等類型的參數。
原子操作:是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另一個線程)。
代碼演示:
說明:通過Interlocked的方法非同步查詢幾個web伺服器,並同時返回數據,且結果只執行一次。
//上報狀態類型 enum CoordinationStatus { Cancel, Timeout, AllDone }
1 class AsyncCoordinator 2 { 3 //AllBegun 內部調用JustEnded來遞減它 4 private int _mOpCount = 1; 5 6 //0=false,1=true 7 private int _mStatusReported = 0; 8 9 private Action<CoordinationStatus> _mCallback; 10 11 private Timer _mTimer; 12 13 //發起一個操作之前調用 14 public void AboutToBegin(int opsToAdd = 1) 15 { 16 Interlocked.Add(ref _mOpCount, opsToAdd); 17 } 18 19 //處理好一個操作的結果之後調用 20 public void JustEnded() 21 { 22 if (Interlocked.Decrement(ref _mOpCount) == 0) 23 { 24 ReportStatus(CoordinationStatus.AllDone); 25 } 26 } 27 28 //該方法必須在發起所有操作後調用 29 public void AllBegin(Action<CoordinationStatus> callback, int timeout = Timeout.Infinite) 30 { 31 _mCallback = callback; 32 if (timeout != Timeout.Infinite) 33 { 34 _mTimer = new Timer(TimeExpired, null, timeout, Timeout.Infinite); 35 JustEnded(); 36 } 37 } 38 39 private void TimeExpired(object o) 40 { 41 ReportStatus(CoordinationStatus.Timeout); 42 } 43 44 public void Cancel() 45 { 46 ReportStatus(CoordinationStatus.Cancel); 47 } 48 49 private void ReportStatus(CoordinationStatus status) 50 { 51 //如果狀態從未報告過,就報告它,否則就忽略它,只調用一次 52 if (Interlocked.Exchange(ref _mStatusReported, 1) == 0) 53 { 54 _mCallback(status); 55 } 56 } 57 }
1 class MultiWebRequest 2 { 3 //輔助類 用於協調所有的非同步操作 4 private AsyncCoordinator _mac = new AsyncCoordinator(); 5 6 protected Dictionary<string,object> _mServers = new Dictionary<string, object> 7 { 8 {"http://www.baidu.com",null},{"http://www.Microsoft.com",null},{"http://www.cctv.com",null}, 9 {"http://www.souhu.com",null},{"http://www.sina.com",null},{"http://www.tencent.com",null}, 10 {"http://www.youku.com",null} 11 }; 12 13 private Stopwatch sp; 14 public MultiWebRequest(int timeout = Timeout.Infinite) 15 { 16 sp = new Stopwatch(); 17 sp.Start(); 18 //通過非同步方式一次性發起請求 19 var httpclient = new HttpClient(); 20 21 foreach (var server in _mServers.Keys) 22 { 23 _mac.AboutToBegin(1); 24 25 httpclient.GetByteArrayAsync(server).ContinueWith(task => ComputeResult(server, task)); 26 } 27 _mac.AllBegin(AllDone,timeout); 28 Console.WriteLine(""); 29 } 30 31 private void ComputeResult(string server, Task<Byte[]> task) 32 { 33 object result; 34 if (task.Exception != null) 35 { 36 result = task.Exception.InnerException; 37 } 38 else 39 { 40 //線程池處理IO 41 result = task.Result.Length; 42 } 43 44 //保存返回結果的長度 45 _mServers[server] = result; 46 47 _mac.JustEnded(); 48 } 49 50 public void Cancel() 51 { 52 _mac.Cancel(); 53 } 54 55 private void AllDone(CoordinationStatus status) 56 { 57 sp.Stop(); 58 Console.WriteLine("響應耗時總計{0}",sp.Elapsed); 59 switch (status) 60 { 61 case CoordinationStatus.Cancel: 62 Console.WriteLine("操作取消"); 63 break; 64 case CoordinationStatus.AllDone: 65 Console.WriteLine("操作完成,完成的結果如下"); 66 foreach (var server in _mServers) 67 { 68 Console.WriteLine("{0}",server.Key); 69 object result = server.Value; 70 if (result is Exception) 71 { 72 Console.WriteLine("錯誤原因{0}",result.GetType().Name); 73 } 74 else 75 { 76 Console.WriteLine("返回位元組數為:{0}",result); 77 } 78 } 79 break; 80 case CoordinationStatus.Timeout: 81 Console.WriteLine("操作超時"); 82 break; 83 default: 84 throw new ArgumentOutOfRangeException("status", status, null); 85 } 86 } 87 }
非常建議大家參考一下以上代碼,我在對伺服器進行訪問時,也會常常參考這個模型。
簡單的自旋鎖
1 class SomeResource 2 { 3 private SimpleSpinLock s1 = new SimpleSpinLock(); 4 5 public void AccessResource() 6 { 7 s1.Enter(); 8 //一次是有一個線程才能進入訪問 9 s1.Leave(); 10 11 } 12 } 13 14 class SimpleSpinLock 15 { 16 private int _mResourceInUse; 17 18 public void Enter() 19 { 20 while (true) 21 { 22 if(Interlocked.Exchange(ref _mResourceInUse,1)==0) 23 return; 24 } 25 } 26 27 public void Leave() 28 { 29 Volatile.Write(ref _mResourceInUse,1); 30 } 31 }
這就是一個線程同步鎖的簡單實現,這種鎖的最大問題在於,存在競爭的情況下會造成線程的“自旋”,這會浪費CPU的寶貴時間,組織CPU做更多的工作,因此,這種自旋鎖應該用於保護那些執行的非常快的代碼。
下篇我們將繼續講解線程同步,內核模式構造和混合線程同步構造,希望這些內容可以幫助到大家一起成長,如果發現博客有什麼錯誤請及時提出寶貴意見,以免造成誤導!