這一篇接著上一篇來繼續學習多線程。
線程同步
在大多數情況下,計算機中的線程會并發運行。有些線程之間沒有聯系,獨立運行,像這種線程我們稱為無關線程。但也有一些線程,之間需要傳遞結果,需要共享資源。像這種線程,我們稱為有關線程。比如,我們網上觀看電影,一個線程負責下載電影,一個線程負責播放電影。它們只有共同合作我們才能觀看到電影,它們之間共享資源。由此,我們可以看出,線程的相關性體現在對同一資源的訪問上。我們把這種供多個線程訪問的資源成為臨界源(Critical Resource)、訪問臨界源的代碼稱為臨界區(Critical Region)。我們看個程序:
//緩沖區,只能容納一個字符 PRivate static char buffer; static void Main(string[] args) { //線程:寫者 Thread writer = new Thread(delegate() { string sentence = "無可奈何花落去,似曾相識燕歸來,小園香徑獨徘徊。"; for (int i = 0; i < 24; i++) { buffer = sentence[i]; //向緩沖區寫入字符 Thread.Sleep(25); } }); //線程:讀者 Thread Reader = new Thread(delegate() { for (int i = 0; i < 24; i++) { char ch = buffer; //從緩存區讀取數據 Console.Write(ch); Thread.Sleep(25); } }); //啟動線程 writer.Start(); Reader.Start(); } }
我們創建兩個線程,一個Writer線程負責向緩存區寫入字符,一個Reader線程負責從緩存區讀取字符。我們假設,緩存區一次只能存放一個字符。也就說,如果Reader不能及時從緩存區讀取字符,那么就會被Writer下次要寫入的字符覆蓋掉。我們來看一下程序的運行效果,如下圖:

出現了混亂,為什么會這樣呢?原因很簡單,因為Writer向緩存中寫入字符,Reader要馬上去讀取,Writer、Reader步調一致,才能得出完整的效果。但是,線程往往是交替執行的,不能確保時間的一致。像這種需要兩個線程協同合作才能完成一項任務的情況叫做線程同步(Synchronization)。如何才能確保兩個線程的同步呢?C#為我們提供了一系列的同步類.
互鎖(Interlocked類)
看程序:
//緩沖區,只能容納一個字符 private static char buffer; //標識量(緩沖區中已使用的空間,初始值為0) private static long numberofUsedSpace = 0;
static void Main(string[] args) { string sentence = "無可奈何花落去,似曾相識燕歸來,小園香徑獨徘徊。"; //線程:寫者 Thread writer = new Thread(delegate() { for (int i = 0; i < 24; i++) { //寫入程序前檢查緩沖區中是否已滿 //如果已滿,就進行等待。如果未滿,就寫入字符. while (Interlocked.Read(ref numberofUsedSpace) == 1) { Thread.Sleep(10); } buffer = sentence[i]; //向緩沖區寫入字符 Thread.Sleep(25); //寫入數據后,將numberofUsedSpace由0變1 Interlocked.Increment(ref numberofUsedSpace); } }); //線程:讀者 Thread Reader = new Thread(delegate() { for (int i = 0; i < 24; i++) { //讀取之前檢查緩沖區是否已滿 //如果已滿,進行讀取,如果未滿,進行等待。 while (Interlocked.Read(ref numberofUsedSpace) == 0) { Thread.Sleep(25); } char ch = buffer; //從緩存區讀取數據 Console.Write(ch); //讀取完字符,將numberofUsedSpace由1設為0 Interlocked.Decrement(ref numberofUsedSpace); } }); //啟動線程 writer.Start(); Reader.Start(); }我們通過一個numerofUsedSpace的變量作為計數器,假設numberofUsedSpace=1已滿,numberofUsedSpace=0未滿。每當Writer線程向緩存區寫入字符時,需要通過Interlocked的Read方法來檢查numberofUsedSpace是否已滿。如果未滿,吸入字符,如果已滿,進行等待。同樣,當Read線程需要向緩沖區讀取字符時,也是通過Interlocked的Rread方法來檢查numberofUsedSpace是否已滿,已滿,進行讀取,未滿進行等待。
管程(Monitor類)
另一種實現線程同步的方法,是通過Monitor類。看程序:
//緩沖區,只能容納一個字符 private static char buffer; //用于同步的對象(獨占鎖) private static object lockForBuffer = new object(); static void Main(string[] args) { //線程:寫者 Thread writer = new Thread(delegate() { string sentence = "無可奈何花落去,似曾相識燕歸來,小園香徑獨徘徊。"; for (int i = 0; i < 24; i++) { try { //進入臨界區 Monitor.Enter(lockForBuffer); buffer = sentence[i]; //向緩沖區寫入字符 //喚醒睡眠在臨界資源上的線程 Monitor.Pulse(lockForBuffer); //讓當前的線程睡眠在臨界資源上 Monitor.Wait(lockForBuffer); } catch (ThreadInterruptedException) { Console.WriteLine("線程writer被中止"); } finally { //推出臨界區 Monitor.Exit(lockForBuffer); } } }); //線程:讀者 Thread Reader = new Thread(delegate() { for (int i = 0; i < 24; i++) { try { //進入臨界區 Monitor.Enter(lockForBuffer); char ch = buffer; //從緩存區讀取數據 Console.Write(ch); //喚醒睡眠在臨界資源上的線程 Monitor.Pulse(lockForBuffer); //讓當前線程睡眠在臨界資源上 Monitor.Wait(lockForBuffer); } catch (ThreadInterruptedException) { Console.WriteLine("線程reader被中止"); } finally { //退出臨界區 Monitor.Exit(lockForBuffer); } } }); //啟動線程 writer.Start(); Reader.Start(); }當線程進入臨界區,會調用Monitor的Entry方法來獲取獨占鎖,如果得到,就進行操作,如果被別的線程占用,就睡眠在臨界資源上,直到獨占鎖被釋放。如果此時,別的線程進入臨界區,會發現獨占鎖被占用,他們會睡眠在臨界資源上。Monitor會記錄有哪些線程睡眠在臨界資源上,當線程執行完操作,調用Pulse()方法,喚醒睡眠在臨界資源上的線程。因為,線程還需要下次操作,所以需要調用Wait()方法,令自己睡眠在臨界資源上。最后通過調用Exit()方法釋放獨占鎖。
Note that:Monitor只能鎖定引用類型的變量,如果使用值類型變量。每調用一次Entry()方法,就進行一次裝箱操作,每進行一次裝箱操作就會得到一個新的object對象。相同的操作執行在不同的對象上,得不得同步的效果。為了確保退出臨界區時臨界資源得到釋放,我們應把Monitor類的代碼放入Try語句,把調用Exit()方法放入finally語句。為了方便,C#為我們提供了更加簡潔的語句。
lock(要鎖定的對象){//臨界區的代碼 。。。。。 。。。。。。}lock語句執行完,會自動調用Exit()方法,來釋放資源。它完全等價于:
try{ Monitor.Entry(要鎖定的對象); //臨界區代碼 。 。。。。。 。。。。。。 。。。。。。}finally{ Monitor.Exit(要鎖定的對象);}當線程以獨占鎖的方式去訪問資源時,其他線程是不能訪問的。只有當lock語句結束后其他線程才可以訪問。從某種方面可以說,lock語句相當于暫定了程序的多線程功能。這就相當于在資源上放了一把鎖,其他線程只能暫定,這樣會使程序的效率大打折扣。所以只有必要時才可以設置獨占鎖。(我們回想一下Interlocked,當通過Interlocked的Read()方法來讀取計數器,如果不符合條件,就會等待,線程狀態變為SleepWaitJoin狀態。但是,Monitor的Entry()方法獲取獨占鎖,如果得不到,線程會被中止,狀態會變為Stopped。這是二者的一點區別)。
互斥體(Mutex類)
在操作系統中,線程往往需要共享資源,而這些資源往往要求排他性的使用。即一次只能由一個線程使用,這種排他性的使用資源稱為線程之間的互斥(Mutual Exclusion)。互斥,從某種角度也起到了線程同步的目的,所以互斥是一種特殊的同步。與Monitor類似,只有獲得Mutex對象的所屬權的線程才可以進入臨界區,沒有獲得所屬權的只能在臨界區外等候。使用Mutex要比使用Monitor消耗資源。但它可以在系統中的不同程序間實現線程同步。
互斥分為局部互斥、系統互斥。顧名思義,局部互斥,只在創建的程序中有效。系統互斥,會在整個系統中有效。
看程序:
static void Main(string[] args) { Thread threadA = new Thread(delegate() { //創建互斥體 Mutex fileMutex = new Mutex(false, "MutexForTimeRecordFile"); string fileName = @"E:/TimeRecord.txt"; for (int i = 1; i <= 10; i++) { try { //請求互斥體的所屬權,若成功,則進入臨界區,若不成功,則等待 fileMutex.WaitOne(); //在臨界區中操作臨界資源,即向文件中寫入數據 File.AppendAllText(fileName, "threadA: " + DateTime.Now + "/r/n"); } catch (ThreadInterruptedException) { Console.WriteLine("線程A被中斷。"); } finally { fileMutex.ReleaseMutex(); //釋放互斥體的所屬權 }
新聞熱點
疑難解答