這一篇接著上一篇來繼續學習多線程。
線程同步
在大多數情況下,計算機中的線程會並發運行。有些線程之間沒有聯系,獨立運行,像這種線程我們稱為無關線程。但也有一些線程,之間需要傳遞結果,需要共享資源。像這種線程,我們稱為有關線程。比如,我們網上觀看電影,一個線程負責下載電影,一個線程負責播放電影。它們只有共同合作我們才能觀看到電影,它們之間共享資源。由此,我們可以看出,線程的相關性體現在對同一資源的訪問上。我們把這種供多個線程訪問的資源成為臨界源(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下次要寫入的字符覆蓋掉。我們來看一下程序的運行效果,如下圖:
//緩沖區,只能容納一個字符 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(); //釋放互斥體的所屬權 } Thread.Sleep(1000); } }); threadA.Start(); }
static void Main(string[] args) { Thread threadB = 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, "ThreadB: " + DateTime.Now + "\r\n"); } catch (ThreadInterruptedException) { Console.WriteLine("線程B被中斷."); } finally { fileMutex.ReleaseMutex(); //釋放互斥體的所屬權 } Thread.Sleep(1000); } }); threadB.Start(); Process.Start("MutecA.exe"); //啟動程序MutexA.exe }
這是兩個程序,我們創建了一個系統互斥體(“MutexForTimeRecordFile”,在整個系統中有效,可以跨程序)。在程序B中有執行程序A的代碼,通過編譯,我們把兩個編譯後的可執行文件放在一起,執行B。效果如下圖:
通過效果圖,我們可以看出,兩個不同的程序通過相同的系統互斥體名,達到了跨程序線程同步的效果。
我們來總結一下C#為我們帶來的這三種實現多線程同步的類。
1.Interlocked類
是通過調用Read()方法,來讀取計數器,判斷計數器,以此達到線程同步的目的。
2.Monitor類
通過調用Entry()方法來獲取獨占鎖,當執行完代碼,要調用Pulse()方法喚醒睡眠在臨界資源上的線程,同時自己調用Wait()方法,睡眠在臨界資源,以便下次訪問臨界區。Monitor與Interlocked的不同點是,當調用Monitor.Entry()方法,未獲得獨占鎖時,線程狀態會變為Stopped。而Interlocked正是將線程處於SleepWaitJoin狀態。
3.Mutex類
調用Mutex的WaitOne()方法來獲取所屬權,獲得所屬權的線程可以進入臨界區,沒有所屬權的線程在臨界區外等待。Mutex對象比Monitor對象消耗資源,但是Mutex對象可以實現跨程序的線程同步。
Mutex分為局部互斥、系統互斥。
淺談java內存模型
不同的平台,內存模型是不一樣的,但是jvm的內存模型規范是統一的。java的多線程並發問題最終都會反映在java的內存模型上,所謂線程安全無非要控制多個線程對某個資源的有序訪問或修改。java的內存模型,要解決兩個主要的問題:可見性和有序性。我們都知道計算機有高速緩存的存在,處理器並不是每次處理數據都是取內存的。JVM定義了自己的內存模型,屏蔽了底層平台內存管理細節,對於java開發人員,要解決的是在jvm內存模型的基礎上,如何解決多線程的可見性和有序性。
那麼,何謂可見性? 多個線程之間是不能互相傳遞數據通信的,它們之間的溝通只能通過共享變量來進行。Java內存模型(JMM)規定了jvm有主內存,主內存是多個線程共享的。當new一個對象的時候,也是被分配在主內存中,每個線程都有自己的工作內存,工作內存存儲了主存的某些對象的副本,當然線程的工作內存大小是有限制的。當線程操作某個對象時,執行順序如下:
(1) 從主存復制變量到當前工作內存 (read and load)
(2) 執行代碼,改變共享變量值 (use and assign)
(3) 用工作內存數據刷新主存相關內容 (store and write) JVM規范定義了線程對主存的操作指令:read,load,use,assign,store,write。當一個共享便變量在多個線程的工作內存中都有副本時,如果一個線程修改了這個共享變量,那麼其他線程應該能夠看到這個被修改後的值,這就是多線程的可見性問題。
那麼,什麼是有序性呢 ?線程在引用變量時不能直接從主內存中引用,如果線程工作內存中沒有該變量,則會從主內存中拷貝一個副本到工作內存中,這個過程為read-load,完成後線程會引用該副本。當同一線程再度引用該字段時,有可能重新從主存中獲取變量副本(read-load-use),也有可能直接引用原來的副本 (use),也就是說 read,load,use順序可以由JVM實現系統決定。
線程不能直接為主存中中字段賦值,它會將值指定給工作內存中的變量副本(assign),完成後這個變量副本會同步到主存儲區(store- write),至於何時同步過去,根據JVM實現系統決定.有該字段,則會從主內存中將該字段賦值到工作內存中,這個過程為read-load,完成後線程會引用該變量副本,當同一線程多次重復對字段賦值時,比如:
for(int i=0;i<10;i++)
a++;
線程有可能只對工作內存中的副本進行賦值,只到最後一次賦值後才同步到主存儲區,所以assign,store,weite順序可以由JVM實現系統決定。假設有一個共享變量x,線程a執行x=x+1。從上面的描述中可以知道x=x+1並不是一個原子操作,它的執行過程如下:
1 從主存中讀取變量x副本到工作內存
2 給x加1
3 將x加1後的值寫回主 存
如果另外一個線程b執行x=x-1,執行過程如下:
1 從主存中讀取變量x副本到工作內存
2 給x減1
3 將x減1後的值寫回主存
那麼顯然,最終的x的值是不可靠的。假設x現在為10,線程a加1,線程b減1,從表面上看,似乎最終x還是為10,但是多線程情況下會有這種情況發生:
1:線程a從主存讀取x副本到工作內存,工作內存中x值為10
2:線程b從主存讀取x副本到工作內存,工作內存中x值為10
3:線程a將工作內存中x加1,工作內存中x值為11
4:線程a將......余下全文>>
給你推薦一些比較好的教程吧,你應該用得著: 漫談C++ Builder多線程編程技術: www.it55.com/...5.html 用MFC編寫多線程程序實例: www.it55.com/...7.html C++寫的web服務器程序(多線程): www.it55.com/...9.html 後面兩個都是多線程的實例教程。