多執行緒程式設計 2 執行緒的同步

2021-05-18 06:55:31 字數 3869 閱讀 9592

在《多執行緒程式設計》系列第一篇講述了如何啟動執行緒,這篇講述執行緒之間存在競爭時如何確保同步並且不發生死鎖。

執行緒不同步引出的問題

下面做乙個假設,假設有100張票,由兩個執行緒來實現乙個售票程式,每次執行緒執行時首先檢查是否還有票未售出,如果有就按照票號從小到大的順序售出票號最小的票,程式的**如下:

這段程式的執行效果並不每次都一樣,下圖是某次執行效果的截圖:

從上圖可以看出票號為001的號被售出了兩次(如果遇上像《無極》中謝霆鋒飾演的那種角色,可能又會引出一場《一張票引發的血案》了,呵呵),為什麼會出現這種情況呢?

請看**③處:

ticketlist.removeat(0);//③

在某個情況有可能執行緒1恰好執行到此處,從ticketlist中取出索引為0的那個元素並將票號輸出,不巧的是正好分給執行緒1執行的時間片已用完,執行緒1進入休眠狀態,執行緒2從頭開始執行,它可以從容地從ticketlist中取出索引為0的那個元素並且將其輸出,因為執行緒1執行的時候雖然輸出了ticketlist中索引為0的那個元素但是來不及將其刪除,所以這時候執行緒2得到的值和上次執行緒1得到的值一致,這就出現了有些票被售出了兩次、有些票可能根本就沒有售出的情況。

出現這種情況的根本原因就是多個執行緒都是對同一資源進行操作所致,所以在多執行緒程式設計應盡可能避免這種情況,當然有些情況下確實避免不了這種情況,這就需要對其採用一些手段來確保不會出現這種情況,這就是所謂的執行緒的同步。

在c#中實現執行緒的同步有幾種方法:lock、mutex、monitor、semaphore、interlocked和readerwriterlock等。同步策略也可以分為同步上下文、同步**區、手動同步幾種方式。

同步上下文

同步上下文的策略主要是依靠synchronizationattribute類來實現。例如下面的**就是乙個實現了上下文同步的類的**:

所有在同乙個上下文域的物件共享同乙個鎖。這樣建立的物件例項屬性、方法和字段就具有執行緒安全性,需要注意的是類的靜態字段、屬性和方法是不具有執行緒安全性的。

同步**區

同步**區是另外一種策略,它是針對特定部分**進行同步的一種方法。

lock同步

針對上面的**,要實現不會出現混亂(兩次賣出同一張票或者有些票根本就沒有賣出),可以lock關鍵字來實現,出現問題的部分就是在於判斷剩餘票數是否大於0,如果大於0則從當前總票數中減去最大的一張票,因此可以對這部分進行處理,**如下:

經過這樣處理之後系統的執行結果就會正常。效果如下:

總的來說,lock語句是一種有效的、不跨越多個方法的小**塊同步的做法,也就是使用lock語句只能在某個方法的部分**之間,不能跨越方法。

monitor類

針對上面的**,如果使用monitor類來同步的話,**則是如下效果:

當然這段**最終執行的效果也和使用lock關鍵字來同步的效果一樣。比較之下,大家會發現使用lock關鍵字來保持同步的差別不大:」lock (objlock)」被換成了」 monitor.exit(objlock);」。實際上如果你通過其它方式檢視最終生成的il**,你會發現使用lock關鍵字的**實際上是用monitor來實現的。

如下**:

實際上是相當於:

我們知道在絕大多數情況下finally中的**塊一定會被執行,這樣確保了即使同步**出現了異常也仍能釋放同步鎖。

monitor類出了enter()和exit()方法之外,還有wait()和pulse()方法。wait()方法是臨時釋放當前活得的鎖,並使當前物件處於阻塞狀態,pulse()方法是通知處於等待狀態的物件可以準備就緒了,它一會就會釋放鎖。下面我們利用這兩個方法來完成乙個協同的執行緒,乙個執行緒負責隨機產生資料,乙個執行緒負責將生成的資料顯示出來。下面是**:

執行上面的**在大部分情況下會看到如下所示的結果:

一般情況下會看到上面的結果,原因是t1的start()方法在先,所以一般會優先活得執行,t1執行後首先獲得物件鎖,然後在迴圈中通過 monitor.wait(lockobject)方法臨時釋放物件鎖,t1這時處於阻塞狀態;這樣t2獲得物件鎖並且得以執行,t2進入迴圈後通過monitor.pulse(lockobject)方法通知等待同乙個物件鎖的t1準備好,然後在生成隨機數之後臨時釋放物件鎖;接著t1獲得了物件鎖,執行輸出t2生成的資料,之後t1通過 monitor.wait(lockobject)通知t2準備就緒,並在下乙個迴圈中通過 monitor.wait(lockobject)方法臨時釋放物件鎖,就這樣t1和t2交替執行,得到了上面的結果。

當然在某些情況下,可能還會看到如下的結果:

至於為什麼會產生這個結果,原因其實很簡單,儘管t1.start()出現在t2.start()之前,但是並不能就認為t1一定會比t2優先執行(儘管可能在大多數情況下是),還要考慮執行緒排程問題,使用了多執行緒之後就會使**的執行順序變得複雜起來。在某種情況下t1和t2對鎖的使用產生了衝突,形成了死鎖,也就出現了如上圖所示的情況,為了避免這種情況可以通過讓t2延時乙個合適的時間。

手控同步

手控同步是指使用不同的同步類來建立自己的同步機制。使用這種策略要求手動地為不同的域或者方法同步。

readerwriterlock

readerwriterlock支援單個寫執行緒和多個讀執行緒的鎖。在任一特定時刻允許多個執行緒同時進行讀操作或者乙個執行緒進行寫操作,使用readerwriterlock來進行讀寫同步比使用監視的方式(如monitor)效率要高。

下面是乙個例子,在例子中使用了兩個讀執行緒和乙個寫執行緒,**如下:

程式的執行效果如下:

waithandle

waithandle類是乙個抽線類,有多個類直接或者間接繼承自waithandle類,類圖如下:

在waithandle類中signalandwait、waitall、waitany及waitone這幾個方法都有過載形式,其中除waitone之外都是靜態的。waithandle方法常用作同步物件的基類。waithandle物件通知其他的執行緒它需要對資源排他性的訪問,其他的執行緒必須等待,直到waithandle不再使用資源和等待控制代碼沒有被使用。

waithandle方法有多個wait的方法,這些方法的區別如下:

waitall:等待指定陣列中的所有元素收到訊號。

waitany:等待指定陣列中的任一元素收到訊號。

waitone:當在派生類中重寫時,阻塞當前執行緒,直到當前的 waithandle 收到訊號。

這些wait方法阻塞執行緒直到乙個或者更多的同步物件收到訊號。

下面的是乙個msdn中的例子,講的是乙個計算過程,最終的計算結果為第一項+第二項+第三項,在計算第

一、二、三項時需要使用基數來進行計算。在**中使用了執行緒池也就是threadpool來操作,這裡面涉及到計算的順序的先後問題,通過waithandle及其子類可以很好地解決這個問題。

**如下:

程式的執行結果如下:

result = 0.355650523270459.

result = 0.125205692112756.

當然因為引入了隨機數,所以每次計算結果並不相同,這裡要講述的是它們之間的控制。首先在 result(int seed)方法中講計算基數、第一項、第二項及第三項的方法放到執行緒池中,要計算第一二三項時首先要確定基數,這些方法通過manualevent.waitone()暫時停止執行,於是計算基數的方法首先執行,計算出基數之後通過manualevent.set()方法通知計算第一二三項的方法開始,在這些方法完成計算之後通過autoevents陣列中的autoresetevent元素的set()方法發出訊號,標識執行完畢。這樣waithandle.waitall(autoevents)這一步可以繼續執行,從而得到執行結果。

在上面**中的waithandle的其它子類限於篇幅不在這裡一一舉例講解,它們在使用了多少有些相似之處(畢竟是乙個爹、從乙個抽象類繼承下來的嘛)。

上傳功能暫時關閉,敬請諒解。

多執行緒程式設計2 執行緒同步

訊號量 訊號量通常有兩種 二進位制訊號量和計數訊號量。二進位制訊號量只有0和1兩種取值,計數訊號量有更大的取值範圍。訊號量一般用來保護一段 使其每次只能被乙個執行執行緒執行,要完成這個工作,可以使用二進位制訊號量。有時,希望可以允許有限數目的執行緒執行一段指定的 這時可以使用計數訊號量。建立 inc...

多執行緒程式設計 2 執行緒的同步

多執行緒程式設計 2 執行緒的同步 分類 c 基礎 2010 01 10 20 18 5463人閱讀 34 收藏舉報 在 多執行緒程式設計 系列第一篇講述了如何啟動執行緒,這篇講述執行緒之間存在競爭時如何確保同步並且不發生死鎖。執行緒不同步引出的問題 下面做乙個假設,假設有100張票,由兩個執行緒來...

Linux多執行緒程式設計(2)執行緒同步

執行緒同步 a.mutex 互斥量 多個執行緒同時訪問共享資料時可能會衝突,這跟前面講訊號時所說的可重要性是同樣的問 題。假如 兩個執行緒都要把某個全域性變數增加1,這個操作在某平台需要三條指令完成 1.從記憶體讀變數值到暫存器 2.暫存器的值加1 3.將暫存器的值寫回記憶體 我們通過乙個簡單的程式...