【转载】dotnet 线程同步
原文:
http://www.cnblogs.com/seerlin/archive/2009/03/10/1407478.html第二部分:線程同步基礎(chǔ)
同步要領(lǐng)
下面的表格列展了.NET對(duì)協(xié)調(diào)或同步線程動(dòng)作的可用的工具:
簡(jiǎn)易阻止方法
| 構(gòu)成 | 目的 |
| Sleep | 阻止給定的時(shí)間周期 |
| Join | 等待另一個(gè)線程完成 |
鎖系統(tǒng)
| 構(gòu)成 | 目的 | 跨進(jìn)程? | 速度 |
| lock | 確保只有一個(gè)線程訪問某個(gè)資源或某段代碼。 | 否 | 快 |
| Mutex | 確保只有一個(gè)線程訪問某個(gè)資源或某段代碼。 | 是 | 中等 |
| Semaphore | 確保不超過指定數(shù)目的線程訪問某個(gè)資源或某段代碼。 | 是 | 中等 |
(同步的情況下也提夠自動(dòng)鎖。)
信號(hào)系統(tǒng)
| 構(gòu)成 | 目的 | 跨進(jìn)程? | 速度 |
| EventWaitHandle | 允許線程等待直到它受到了另一個(gè)線程發(fā)出信號(hào)。 | 是 | 中等 |
| Wait 和 Pulse* | 允許一個(gè)線程等待直到自定義阻止條件得到滿足。 | 否 | 中等 |
非阻止同步系統(tǒng)*
| 構(gòu)成 | 目的 | 跨進(jìn)程? | 速度 |
| Interlocked* | 完成簡(jiǎn)單的非阻止原子操作。 | 是(內(nèi)存共享情況下) | 非常快 |
| volatile* | 允許安全的非阻止在鎖之外使用個(gè)別字段。 | 非常快 |
* 代表頁面將轉(zhuǎn)到第四部分
阻止 (Blocking)
當(dāng)一個(gè)線程通過上面所列的方式處于等待或暫停的狀態(tài),被稱為被阻止。一旦被阻止,線程立刻放棄它被分配的CPU時(shí)間,將它的ThreadState屬性添加為WaitSleepJoin狀態(tài),不在安排時(shí)間直到停止阻止。停止阻止在任意四種情況下發(fā)生(關(guān)掉電腦的電源可不算!):
- 阻止的條件已得到滿足
- 操作超時(shí)(如果timeout被指定了)
- 通過Thread.Interrupt中斷了
- 通過Thread.Abort放棄了
當(dāng)線程通過(不建議)Suspend?方法暫停,不認(rèn)為是被阻止了。
休眠 和 輪詢
調(diào)用Thread.Sleep阻止當(dāng)前的線程指定的時(shí)間(或者直到中斷):
static void Main() { Thread.Sleep (0); // 釋放CPU時(shí)間片 Thread.Sleep (1000); // 休眠1000毫秒 Thread.Sleep (TimeSpan.FromHours (1)); // 休眠1小時(shí) Thread.Sleep (Timeout.Infinite); // 休眠直到中斷 }更確切地說,Thread.Sleep放棄了占用CPU,請(qǐng)求不在被分配時(shí)間直到給定的時(shí)間經(jīng)過。Thread.Sleep(0)放棄CPU的時(shí)間剛剛夠其它在時(shí)間片隊(duì)列里的活動(dòng)線程(如果有的話)被執(zhí)行。
Thread.Sleep在阻止方法中是唯一的暫停汲取Windows Forms程序的Windows消息的方法,或COM環(huán)境中用于單元模式。 這在Windows Forms程序中是一個(gè)很大的問題,任何對(duì)主UI線程的阻止都將使程序失去相應(yīng)。因此一般避免這樣使用,無論信息汲取是否被“技術(shù)地”暫定與否。由COM 遺留下來的宿主環(huán)境更為復(fù)雜,在一些時(shí)候它決定停止,而卻保持信息的汲取存活。微軟的 Chris Brumm 在他的博客中討論這個(gè)問題。(搜索: 'COM "Chris Brumme"')
線程類同時(shí)也提供了一個(gè)SpinWait方法,它使用輪詢CPU而非放棄CPU時(shí)間的方式,保持給定的迭代次數(shù)進(jìn)行“無用地繁忙”。50迭代可能等同于停頓大約一微秒,雖然這將取決于CPU的速度和負(fù)載。從技術(shù)上講,SpinWait并不是一個(gè)阻止的方法:一個(gè)處于spin-waiting的線程的ThreadState不是WaitSleepJoin狀態(tài),并且也不會(huì)被其它的線程過早的中斷(Interrupt)。SpinWait很少被使用,它的作用是等待一個(gè)在極短時(shí)間(可能小于一微秒)內(nèi)可準(zhǔn)備好的可預(yù)期的資源,而不用調(diào)用Sleep方法阻止線程而浪費(fèi)CPU時(shí)間。不過,這種技術(shù)的優(yōu)勢(shì)只有在多處理器計(jì)算機(jī):對(duì)單一處理器的電腦,直到輪詢的線程結(jié)束了它的時(shí)間片之前,一個(gè)資源沒有機(jī)會(huì)改變狀態(tài),這有違它的初衷。并且調(diào)用SpinWait經(jīng)常會(huì)花費(fèi)較長(zhǎng)的時(shí)間這本身就浪費(fèi)了CPU時(shí)間。
阻止 vs. 輪詢
線程可以等待某個(gè)確定的條件來明確輪詢使用一個(gè)輪詢的方式,比如:
while (!proceed);或者:
while (DateTime.Now < nextStartTime);這是非常浪費(fèi)CPU時(shí)間的:對(duì)于CLR和操作系統(tǒng)而言,線程進(jìn)行了一個(gè)重要的計(jì)算,所以分配了相應(yīng)的資源!在這種狀態(tài)下的輪詢線程不算是阻止,不像一個(gè)線程等待一個(gè)EventWaitHandle(一般使用這樣的信號(hào)任務(wù)來構(gòu)建)。
阻止和輪詢組合使用可以產(chǎn)生一些變換:
while (!proceed) Thread.Sleep (x); // "輪詢休眠!"x越大,CPU效率越高,折中方案是增大潛伏時(shí)間,任何20ms的花費(fèi)是微不足道的,除非循環(huán)中的條件是極其復(fù)雜的。
除了稍有延遲,這種輪詢和休眠的方式可以結(jié)合的非常好。(但有并發(fā)問題,在第四部分討論)可能它最大的用處在于程序員可以放棄使用復(fù)雜的信號(hào)結(jié)構(gòu)?來工作了。
使用Join等待一個(gè)線程完成
你可以通過Join方法阻止線程直到另一個(gè)線程結(jié)束:
class JoinDemo { static void Main() { Thread t = new Thread (delegate() { Console.ReadLine(); }); t.Start(); t.Join(); // 等待直到線程完成 Console.WriteLine ("Thread t's ReadLine complete!"); } }Join方法也接收一個(gè)使用毫秒或用TimeSpan類的超時(shí)參數(shù),當(dāng)Join超時(shí)是返回false,如果線程已終止,則返回true 。Join所帶的超時(shí)參數(shù)非常像Sleep方法,實(shí)際上下面兩行代碼幾乎差不多:
Thread.Sleep (1000); Thread.CurrentThread.Join (1000);(他們的區(qū)別明顯在于單線程的應(yīng)用程序域與COM互操作性,源于先前描述Windows信息汲取部分:在阻止時(shí),Join保持信息汲取,Sleep暫停信息汲取。)
鎖和線程安全
鎖實(shí)現(xiàn)互斥的訪問,被用于確保在同一時(shí)刻只有一個(gè)線程可以進(jìn)入特殊的代碼片段,考慮下面的類:
class ThreadUnsafe { ? static int val1, val2; ? ? static void Go() { ??? if (val2 != 0) Console.WriteLine (val1 / val2); ??? val2 = 0; ? } }這不是線程安全的:如果Go方法被兩個(gè)線程同時(shí)調(diào)用,可能會(huì)得到在某個(gè)線程中除數(shù)為零的錯(cuò)誤,因?yàn)関al2可能被一個(gè)線程設(shè)置為零,而另一個(gè)線程剛好執(zhí)行到if和Console.WriteLine語句。
下面用lock來修正這個(gè)問題:
class ThreadSafe { ? static object locker = new object(); ? static int val1, val2; ? ? static void Go() { ??? lock (locker) { ????? if (val2 != 0) Console.WriteLine (val1 / val2); ????? val2 = 0; ??? } ? } }在同一時(shí)刻只有一個(gè)線程可以鎖定同步對(duì)象(在這里是locker),任何競(jìng)爭(zhēng)的的其它線程都將被阻止,直到這個(gè)鎖被釋放。如果有大于一個(gè)的線程競(jìng)爭(zhēng)這個(gè)鎖,那么他們將形成稱為“就緒隊(duì)列”的隊(duì)列,以先到先得的方式授權(quán)鎖。互斥鎖有時(shí)被稱之對(duì)由鎖所保護(hù)的內(nèi)容強(qiáng)迫串行化訪問,因?yàn)橐粋€(gè)線程的訪問不能與另一個(gè)重疊。在這個(gè)例子中,我們保護(hù)了Go方法的邏輯,以及val1?和val2字段的邏輯。
一個(gè)等候競(jìng)爭(zhēng)鎖的線程被阻止將在ThreadState上為WaitSleepJoin狀態(tài)。稍后我們將討論一個(gè)線程通過另一個(gè)線程調(diào)用Interrupt或Abort方法來強(qiáng)制地被釋放。這是一個(gè)相當(dāng)高效率的技術(shù)可以被用于結(jié)束工作線程。
C#的lock?語句實(shí)際上是調(diào)用Monitor.Enter和Monitor.Exit,中間夾雜try-finally語句的簡(jiǎn)略版,下面是實(shí)際發(fā)生在之前例子中的Go方法:
Monitor.Enter (locker); try { ? if (val2 != 0) Console.WriteLine (val1 / val2); ? val2 = 0; } finally { Monitor.Exit (locker); }? ?在同一個(gè)對(duì)象上,在調(diào)用第一個(gè)之前Monitor.Enter而先調(diào)用了Monitor.Exit將引發(fā)異常。
Monitor?也提供了TryEnter方法來實(shí)現(xiàn)一個(gè)超時(shí)功能——也用毫秒或TimeSpan,如果獲得了鎖返回true,反之沒有獲得返回false,因?yàn)槌瑫r(shí)了。TryEnter也可以沒有超時(shí)參數(shù),“測(cè)試”一下鎖,如果鎖不能被獲取的話就立刻超時(shí)。
選擇同步對(duì)象
任何對(duì)所有有關(guān)系的線程都可見的對(duì)象都可以作為同步對(duì)象,但要服從一個(gè)硬性規(guī)定:它必須是引用類型。也強(qiáng)烈建議同步對(duì)象最好私有在類里面(比如一個(gè)私有實(shí)例字段)防止無意間從外部鎖定相同的對(duì)象。服從這些規(guī)則,同步對(duì)象可以兼對(duì)象和保護(hù)兩種作用。比如下面List?:
class ThreadSafe { ? List <string> list = new List <string>(); ? void Test() { ??? lock (list) { list.Add ("Item 1"); ...一個(gè)專門字段是常用的(如在先前的例子中的locker) , 因?yàn)樗梢跃_控制鎖的范圍和粒度。用對(duì)象或類本身的類型作為一個(gè)同步對(duì)象,即:
lock (this) { ... }或:
lock (typeof (Widget)) { ... }??? // 保護(hù)訪問靜態(tài)是不好的,因?yàn)檫@潛在的可以在公共范圍訪問這些對(duì)象。
鎖并沒有以任何方式阻止對(duì)同步對(duì)象本身的訪問,換言之,x.ToString()不會(huì)由于另一個(gè)線程調(diào)用lock(x)?而被阻止,兩者都要調(diào)用lock(x)?來完成阻止工作。
嵌套鎖定
線程可以重復(fù)鎖定相同的對(duì)象,可以通過多次調(diào)用Monitor.Enter或lock語句來實(shí)現(xiàn)。當(dāng)對(duì)應(yīng)編號(hào)的Monitor.Exit被調(diào)用或最外面的lock語句完成后,對(duì)象那一刻被解鎖。這就允許最簡(jiǎn)單的語法實(shí)現(xiàn)一個(gè)方法的鎖調(diào)用另一個(gè)鎖:
static object x = new object(); ? static void Main() { ? lock (x) { ???? Console.WriteLine ("I have the lock"); ???? Nest(); ???? Console.WriteLine ("I still have the lock"); ? } ? 在這鎖被釋放 } ? static void Nest() { ? lock (x) { ??? ... ??} ? 釋放了鎖?沒有完全釋放! }線程只能在最開始的鎖或最外面的鎖時(shí)被阻止。
何時(shí)進(jìn)行鎖定
作為一項(xiàng)基本規(guī)則,任何和多線程有關(guān)的會(huì)進(jìn)行讀和寫的字段應(yīng)當(dāng)加鎖。甚至是極平常的事情——單一字段的賦值操作,都必須考慮到同步問題。在下面的例子中Increment和Assign?都不是線程安全的:
class ThreadUnsafe { ? static int x; ? static void Increment() { x++; } ? static void Assign() { x = 123; } }下面是Increment?和?Assign?線程安全的版本:
class ThreadUnsafe { static object locker = new object(); ? static int x; ? ? static void Increment() { lock (locker) x++; } ? static void Assign() { lock (locker) x = 123; } }作為鎖定另一個(gè)選擇,在一些簡(jiǎn)單的情況下,你可以使用非阻止同步,在第四部分討論(即使像這樣的語句需要同步的原因)。
鎖和原子操作
如果有很多變量在一些鎖中總是進(jìn)行讀和寫的操作,那么你可以稱之為原子操作。我們假設(shè)x?和?y不停地讀和賦值,他們?cè)阪i內(nèi)通過locker鎖定:
lock (locker) { if (x != 0) y /= x; }你可以認(rèn)為x?和?y?通過原子的方式訪問,因?yàn)榇a段沒有被其它的線程分開?或?搶占,別的線程改變x?和?y是無效的輸出,你永遠(yuǎn)不會(huì)得到除數(shù)為零的錯(cuò)誤,保證了x?和?y總是被相同的排他鎖訪問。
性能考量
鎖 定本身是非常快的,一個(gè)鎖在沒有堵塞的情況下一般只需幾十納秒(十億分之一秒)。如果發(fā)生堵塞,任務(wù)切換帶來的開銷接近于數(shù)微秒(百萬分之一秒)的范圍 內(nèi),盡管在線程重組實(shí)際的安排時(shí)間之前它可能花費(fèi)數(shù)毫秒(千分之一秒)。而相反,與此相形見絀的是該使用鎖而沒使用的結(jié)果就是帶來數(shù)小時(shí)的時(shí)間,甚至超 時(shí)。
如 果耗盡并發(fā),鎖定會(huì)帶來反作用,死鎖和爭(zhēng)用鎖,耗盡并發(fā)由于太多的代碼被放置到鎖語句中了,引起其它線程不必要的被阻止。死鎖是兩線程彼此等待被鎖定的內(nèi) 容,導(dǎo)致兩者都無法繼續(xù)下去。爭(zhēng)用鎖是兩個(gè)線程任一個(gè)都可以鎖定某個(gè)內(nèi)容,如果“錯(cuò)誤”的線程獲取了鎖,則導(dǎo)致程序錯(cuò)誤。
對(duì)于太多的同步對(duì)象死鎖是非常容易出現(xiàn)的癥狀,一個(gè)好的規(guī)則是開始于較少的鎖,在一個(gè)可信的情況下涉及過多的阻止出現(xiàn)時(shí),增加鎖的粒度。
線程安全
線程安全的代碼是指在面對(duì)任何多線程情況下,這代碼都沒有不確定的因素。線程安全首先完成鎖,然后減少在線程間交互的可能性。
一個(gè)線程安全的方法,在任何情況下可以可重入式調(diào)用。通用類型在它們中很少是線程安全的,原因如下:
- 完全線程安全的開發(fā)是重要的,尤其是一個(gè)類型有很多字段(在任意多線程上下文中每個(gè)字段都有潛在的交互作用)的情況下。
- 線程安全帶來性能損失(要付出的,在某種程度上無論與否類型是否被用于多線程)。
- 一個(gè)線程安全類型不一定能使程序使用線程安全,有時(shí)參與工作后者可使前者變得冗余。
因此線程安全經(jīng)常只在需要實(shí)現(xiàn)的地方來實(shí)現(xiàn),為了處理一個(gè)特定的多線程情況。
不 過,有一些方法來“欺騙”,有龐大和復(fù)雜的類安全地運(yùn)行在多線程環(huán)境中。一種是犧牲粒度包含大段的代碼——甚至在排他鎖中訪問全局對(duì)象,迫使在更高的級(jí)別 上實(shí)現(xiàn)串行化訪問。這一策略也很關(guān)鍵,讓非線程安全的對(duì)象用于線程安全代碼中,避免了相同的互斥鎖被用于保護(hù)對(duì)在非線程安全對(duì)象的所有的屬性、方法和字段 的訪問。
原始類型除外,很少的.NET framework類型實(shí)例相比于并發(fā)的只讀訪問,是線程安全的。責(zé)任在開放人員實(shí)現(xiàn)線程安全代表性地使用互斥鎖。
另 一個(gè)方式欺騙是通過最小化共享數(shù)據(jù)來最小化線程交互。這是一個(gè)很好的途徑,被暗中地用于“弱狀態(tài)”的中間層程序和web服務(wù)器。自多個(gè)客戶端請(qǐng)求同時(shí)到 達(dá),每個(gè)請(qǐng)求來自它自己的線程(效力于ASP.NET,Web服務(wù)器或者遠(yuǎn)程體系結(jié)構(gòu)),這意味著它們調(diào)用的方法一定是線程安全的。弱狀態(tài)設(shè)計(jì)(因伸縮性 好而流行)本質(zhì)上限制了交互的能力,因此類不能夠在每個(gè)請(qǐng)求間持久保留數(shù)據(jù)。線程交互僅限于可以被選擇創(chuàng)建的靜態(tài)字段,多半是在內(nèi)存里緩存常用數(shù)據(jù)和提供 基礎(chǔ)設(shè)施服務(wù),例如認(rèn)證和審核。
線程安全與.NET Framework類型
鎖定可被用于將非線程安全的代碼轉(zhuǎn)換成線程安全的代碼。好的例子是在.NET framework方面,幾乎所有非初始類型的實(shí)例都不是線程安全的,而如果所有的訪問給定的對(duì)象都通過鎖進(jìn)行了保護(hù)的話,他們可以被用于多線程代碼中。看這個(gè)例子,兩個(gè)線程同時(shí)為相同的List增加條目,然后枚舉它:
class ThreadSafe { ? static List <string> list = new List <string>(); ? ? static void Main() { ??? new Thread (AddItems).Start(); ??? new Thread (AddItems).Start(); ? } ? ? static void AddItems() { ??? for (int i = 0; i < 100; i++) ????? lock (list) ??????? list.Add ("Item " + list.Count); ? ??? string[] items; ??? lock (list) items = list.ToArray(); ??? foreach (string s in items) Console.WriteLine (s); ??} }在這種情況下,我們鎖定了list對(duì)象本身,這個(gè)簡(jiǎn)單的方案是很好的。如果我們有兩個(gè)相關(guān)的list,也許我們就要鎖定一個(gè)共同的目標(biāo)——可能是單獨(dú)的一個(gè)字段,如果沒有其它的list出現(xiàn),顯然鎖定它自己是明智的選擇。
枚舉.NET的集合也不是線程安全的,在枚舉的時(shí)候另一個(gè)線程改動(dòng)list的話,會(huì)拋出異常。勝于直接鎖定枚舉過程,在這個(gè)例子中,我們首先將項(xiàng)目復(fù)制到數(shù)組當(dāng)中,這就避免了固定住鎖因?yàn)槲覀冊(cè)诿杜e過程中有潛在的耗時(shí)。
這里的一個(gè)有趣的假設(shè):想象如果List實(shí)際上為線程安全的,如何解決呢?代碼會(huì)很少!舉例說明,我們說我們要增加一個(gè)項(xiàng)目到我們假象的線程安全的list里,如下:
if (!myList.Contains (newItem)) myList.Add (newItem);無論與否list是否為線程安全的,這個(gè)語句顯然不是!整個(gè)if語句必須放到一個(gè)鎖中,用來保護(hù)搶占在判斷有無和增加新的之間。上述的鎖需要用于任何我們需要修改list的地方,比如下面的語句需要被同樣的鎖包括住:
myList.Clear();來保證它沒有搶占之前的語句,換言之,我們必須鎖定差不多所有非線程安全的集合類們。內(nèi)置的線程安全,顯而易見是浪費(fèi)時(shí)間!
在寫自定義組件的時(shí)候,你可能會(huì)反對(duì)這個(gè)觀點(diǎn)——為什么建造線程安全讓它容易的結(jié)果會(huì)變的多余呢 ?
有一個(gè)爭(zhēng)論:在一個(gè)對(duì)象包上自定義的鎖僅在所有并行的線程知道、并且使用這個(gè)鎖的時(shí)候才能工作,而如果鎖對(duì)象在更大的范圍內(nèi)的時(shí)候,這個(gè)鎖對(duì)象可能不在這個(gè)鎖范圍內(nèi)。最糟糕的情況是靜態(tài)成員在公共類型中出現(xiàn)了,比如,想象靜態(tài)結(jié)構(gòu)在DateTime上,DateTime.Now不是線程安全的,當(dāng)有2個(gè)并發(fā)的調(diào)用可帶來錯(cuò)亂的輸出或異常,補(bǔ)救方式是在其外進(jìn)行鎖定,可能鎖定它的類型本身——?lock(typeof(DateTime))來圈住調(diào)用DateTime.Now,這會(huì)工作的,但只有所有的程序員同意這樣做的時(shí)候。然而這并靠不住,鎖定一個(gè)類型被認(rèn)為是一件非常不好的事情。
由于這些理由,DateTime上的靜態(tài)成員是保證線程安全的,這是一個(gè)遍及.NET framework一個(gè)普遍模式——靜態(tài)成員是線程安全的,而一個(gè)實(shí)例成員則不是。從這個(gè)模式也能在寫自定義類型時(shí)得到一些體會(huì),不要?jiǎng)?chuàng)建一個(gè)不能線程安全的難題!
當(dāng)寫公用組件的時(shí)候,好的習(xí)慣是不要忘記了線程安全,這意味著要單獨(dú)小心處理那些在其中或公共的靜態(tài)成員。
Interrupt 和 Abort
一個(gè)被阻止的線程可以通過兩種方式被提前釋放:
- 通過?Thread.Interrupt
- 通過?Thread.Abort
這必須通過另外活動(dòng)的線程實(shí)現(xiàn),等待的線程是沒有能力對(duì)它的被阻止?fàn)顟B(tài)做任何事情的。
Interrupt方法
在一個(gè)被阻止的線程上調(diào)用Interrupt?方法,將強(qiáng)迫釋放它,拋出ThreadInterruptedException異常,如下:
class Program { ? static void Main() { ??? Thread t = new Thread (delegate() { ????? try { ??????? Thread.Sleep (Timeout.Infinite); ????? } ????? catch (ThreadInterruptedException) { ??????? Console.Write ("Forcibly "); ????? } ????? Console.WriteLine ("Woken!"); ??? }); ? ??? t.Start(); ??? t.Interrupt(); ? } }Forcibly Woken!
中斷一個(gè)線程僅僅釋放它的當(dāng)前的(或下一個(gè))等待狀態(tài):它并不結(jié)束這個(gè)線程(當(dāng)然,除非未處理ThreadInterruptedException異常)。
如果Interrupt被一個(gè)未阻止的線程調(diào)用,那么線程將繼續(xù)執(zhí)行直到下一次被阻止時(shí),它拋出ThreadInterruptedException異常。用下面的測(cè)試避免這個(gè)問題:
if ((worker.ThreadState & ThreadState.WaitSleepJoin) > 0) ? worker.Interrupt();這不是一個(gè)線程安全的方式,因?yàn)榭赡鼙粨屨剂嗽趇f語句和worker.Interrupt間。
隨 意中斷線程是危險(xiǎn)的,因?yàn)槿魏慰蚣芑虻谌椒椒ㄔ谡{(diào)用堆棧時(shí)可能會(huì)意外地在已訂閱的代碼上收到中斷。這一切將被認(rèn)為是線程被暫時(shí)阻止在一個(gè)鎖中或同步資源 中,并且所有掛起的中斷將被踢開。如果這個(gè)方法沒有被設(shè)計(jì)成可以被中斷(沒有適當(dāng)處理finally塊)的對(duì)象可能剩下無用的狀態(tài),或資源不完全地被釋 放。
中斷一個(gè)線程是安全的,當(dāng)你知道它確切的在哪的時(shí)候。稍后我們討論?信號(hào)系統(tǒng),它提供這樣的一種方式。
Abort方法
被阻止的線程也可以通過Abort方法被強(qiáng)制釋放,這與調(diào)用Interrupt相似,除了用ThreadAbortException異常代替了ThreadInterruptedException異常,此外,異常將被重新拋出在catch里(在試圖以有好方式處理異常的時(shí)候),直到Thread.ResetAbort在catch中被調(diào)用;在這期間線程的ThreadState為AbortRequested。
在Interrupt?與?Abort?之間最大不同在于它們調(diào)用一個(gè)非阻止線程所發(fā)生的事情。Interrupt繼續(xù)工作直到下一次阻止發(fā)生,Abort在線程當(dāng)前所執(zhí)行的位置(可能甚至不在你的代碼中)拋出異常。終止一個(gè)非阻止的線程會(huì)帶來嚴(yán)重的后果,這在后面的 “終止線程”章節(jié)中將詳細(xì)討論。
線程狀態(tài)
圖1: 線程狀態(tài)關(guān)系圖
你可以通過ThreadState屬性獲取線程的執(zhí)行狀態(tài)。圖1將ThreadState列舉為“層”。ThreadState被設(shè)計(jì)的很恐怖,它以按位計(jì)算的方式組合三種狀態(tài)“層”,每種狀態(tài)層的成員它們間都是互斥的,下面是所有的三種狀態(tài)“層”:
- 運(yùn)行 (running) / 阻止 (blocking) / 終止 (aborting) 狀態(tài)(圖1顯示)
- 后臺(tái) (background) / 前臺(tái) (foreground) 狀態(tài) (ThreadState.Background)
- 不建議使用的Suspend?方法(ThreadState.SuspendRequested?和?ThreadState.Suspended)掛起的過程
總的來說,ThreadState是按位組合零或每個(gè)狀態(tài)層的成員!一個(gè)簡(jiǎn)單的ThreadState例子:
Unstarted Running WaitSleepJoin Background, Unstarted SuspendRequested, Background, WaitSleepJoin(所枚舉的成員有兩個(gè)從來沒被用過,至少是當(dāng)前CLR實(shí)現(xiàn)上:StopRequested?和?Aborted。)
還有更加復(fù)雜的,ThreadState.Running潛在的值為0 ,因此下面的測(cè)試不工作:
if ((t.ThreadState & ThreadState.Running) > 0) ...你必須用按位與非操作符來代替,或者使用線程的IsAlive屬性。但是IsAlive可能不是你想要的,它在被阻止或掛起的時(shí)候返回true(只有在線程未開始或已結(jié)束時(shí)它才為true)。
假設(shè)你避開不推薦使用的Suspend?和?Resume方法,你可以寫一個(gè)helper方法除去所有除了第一種狀態(tài)層的成員,允許簡(jiǎn)單測(cè)試計(jì)算完成。線程的后臺(tái)狀態(tài)可以通過IsBackground?獨(dú)立地獲得,所以實(shí)際上只有第一種狀態(tài)層擁有有用的信息。
public static ThreadState SimpleThreadState (ThreadState ts) { ? return ts & (ThreadState.Aborted | ThreadState.AbortRequested | ??????????? ThreadState.Stopped | ThreadState.Unstarted | ??????????? ThreadState.WaitSleepJoin); }ThreadState對(duì)調(diào)試或程序概要分析是無價(jià)之寶,與之不相稱的是多線程的協(xié)同工作,因?yàn)闆]有一個(gè)機(jī)制存在:通過判斷ThreadState來執(zhí)行信息,而不考慮ThreadState期間的變化。
等待句柄
lock語句(也稱為Monitor.Enter?/?Monitor.Exit)是線程同步結(jié)構(gòu)的一個(gè)例子。當(dāng)lock對(duì)一段代碼或資源實(shí)施排他訪問時(shí), 有些同步任務(wù)是笨拙的或難以實(shí)現(xiàn)的,比如說傳輸信號(hào)給等待的工作線程開始任務(wù)。
Win32 API擁有豐富的同步系統(tǒng),這在.NET framework以EventWaitHandle,?Mutex?和?Semaphore類展露出來。而一些比有些更有用:例如Mutex類,在EventWaitHandle提供唯一的信號(hào)功能時(shí),大多會(huì)成倍提高lock的效率。
這三個(gè)類都依賴于WaitHandle類,盡管從功能上講, 它們相當(dāng)?shù)牟煌5鼈冏龅氖虑槎加幸粋€(gè)共同點(diǎn),那就是,被“點(diǎn)名”,這允許它們繞過操作系統(tǒng)進(jìn)程工作,而不是只能在當(dāng)前進(jìn)程里繞過線程。
EventWaitHandle有兩個(gè)子類:AutoResetEvent?和?ManualResetEvent(不涉及到C#中的事件或委托)。這兩個(gè)類都派生自它們的基類:它們僅有的不同是它們用不同的參數(shù)調(diào)用基類的構(gòu)造函數(shù)。
性能方面,使用Wait Handles系統(tǒng)開銷會(huì)花費(fèi)在較小微秒間,不會(huì)在它們使用的上下文中產(chǎn)生什么后果。
AutoResetEvent在WaitHandle中是最有用的的類,它連同lock 語句是一個(gè)主要的同步結(jié)構(gòu)。
AutoResetEvent
AutoResetEvent就像一個(gè)用票通過的旋轉(zhuǎn)門:插入一張票,讓正確的人通過。類名字里的“auto”實(shí)際上就是旋轉(zhuǎn)門自動(dòng)關(guān)閉或“重新安排”后來的人讓其通過。一個(gè)線程等待或阻止通過在門上調(diào)用WaitOne方法(直到等到這個(gè)“one”,門才開) ,票的插入則由調(diào)用Set方法。如果由許多線程調(diào)用WaitOne,在門前便形成了隊(duì)列,一張票可能來自任意某個(gè)線程——換言之,任何(非阻止)線程要通過AutoResetEvent對(duì)象調(diào)用Set方法來釋放一個(gè)被阻止的的線程。
如果Set調(diào)用時(shí)沒有任何線程處于等待狀態(tài),那么句柄保持打開直到某個(gè)線程調(diào)用了WaitOne?。這個(gè)行為避免了在線程起身去旋轉(zhuǎn)門和線程插入票(哦,插入票是非常短的微秒間的事,真倒霉,你將必須不確定地等下去了!)間的競(jìng)爭(zhēng)。但是在沒人等的時(shí)候重復(fù)地在門上調(diào)用Set方法不會(huì)允許在一隊(duì)人都通過,在他們到達(dá)的時(shí)候:僅有下一個(gè)人可以通過,多余的票都被“浪費(fèi)了"。
WaitOne?接受一個(gè)可選的超時(shí)參數(shù)——當(dāng)?shù)却猿瑫r(shí)結(jié)束時(shí)這個(gè)方法將返回false,WaitOne在等待整段時(shí)間里也通知離開當(dāng)前的同步內(nèi)容,為了避免過多的阻止發(fā)生。
Reset方法提供在沒有任何等待或阻止的時(shí)候關(guān)閉旋轉(zhuǎn)門——它應(yīng)該是開著的。
AutoResetEvent可以通過2種方式創(chuàng)建,第一種是通過構(gòu)造函數(shù):
EventWaitHandle wh = new AutoResetEvent (false);如果布爾參數(shù)為真,Set方法在構(gòu)造后立刻被自動(dòng)的調(diào)用,另一個(gè)方法是通過它的基類EventWaitHandle:
EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto);EventWaitHandle的構(gòu)造器也允許創(chuàng)建ManualResetEvent(用EventResetMode.Manual定義).
在Wait Handle不在需要時(shí)候,你應(yīng)當(dāng)調(diào)用Close方法來釋放操作系統(tǒng)資源。但是,如果一個(gè)Wait Handle將被用于程序(就像這一節(jié)的大多例子一樣)的生命周期中,你可以發(fā)點(diǎn)懶省略這個(gè)步驟,它將在程序域銷毀時(shí)自動(dòng)的被銷毀。
接下來這個(gè)例子,一個(gè)線程開始等待直到另一個(gè)線程發(fā)出信號(hào)。
class BasicWaitHandle { ? static EventWaitHandle wh = new AutoResetEvent (false); ? ? static void Main() { ??? new Thread (Waiter).Start(); ??? Thread.Sleep (1000);????????????????? // 等一會(huì)... ??? wh.Set();???????????????????????? ????// OK ——喚醒它 ? } ? ??static void Waiter() { ??? Console.WriteLine ("Waiting..."); ??? wh.WaitOne();??????????????????????? // 等待通知 ??? Console.WriteLine ("Notified"); ? } }Waiting...?(pause)?Notified.
創(chuàng)建跨進(jìn)程的EventWaitHandle
EventWaitHandle的構(gòu)造器允許以“命名”的方式進(jìn)行創(chuàng)建,它有能力跨多個(gè)進(jìn)程。名稱是個(gè)簡(jiǎn)單的字符串,可能會(huì)無意地與別的沖突!如果名字使用了,你將引用相同潛在的EventWaitHandle,除非操作系統(tǒng)創(chuàng)建一個(gè)新的,看這個(gè)例子:
EventWaitHandle wh = new EventWaitHandle (false, EventResetMode.Auto, ? "MyCompany.MyApp.SomeName");如果有兩個(gè)程序都運(yùn)行這段代碼,他們將彼此可以發(fā)送信號(hào),等待句柄可以跨這兩個(gè)進(jìn)程中的所有線程。
任務(wù)確認(rèn)
設(shè) 想我們希望在后臺(tái)完成任務(wù),不在每次我們得到任務(wù)時(shí)再創(chuàng)建一個(gè)新的線程。我們可以通過一個(gè)輪詢的線程來完成:等待一個(gè)任務(wù),執(zhí)行它,然后等待下一個(gè)任務(wù)。 這是一個(gè)普遍的多線程方案。也就是在創(chuàng)建線程上切分內(nèi)務(wù)操作,任務(wù)執(zhí)行被序列化,在多個(gè)工作線程和過多的資源消耗間排除潛在的不想要的操作。
我們必須決定要做什么,但是,如果當(dāng)新的任務(wù)來到的時(shí)候,工作線程已經(jīng)在忙之前的任務(wù)了,設(shè)想這種情形下我們需選擇阻止調(diào)用者直到之前的任務(wù)被完成。像這樣的系統(tǒng)可以用兩個(gè)AutoResetEvent對(duì)象實(shí)現(xiàn):一個(gè)“ready”AutoResetEvent,當(dāng)準(zhǔn)備好的時(shí)候,它被工作線程調(diào)用Set方法;和“go”AutoResetEvent,當(dāng)有新任務(wù)的時(shí)候,它被調(diào)用線程調(diào)用Set方法。在下面的例子中,一個(gè)簡(jiǎn)單的string字段被用于決定任務(wù)(使用了volatile?關(guān)鍵字聲明,來確保兩個(gè)線程都可以看到相同版本):
class AcknowledgedWaitHandle { ? static EventWaitHandle ready = new AutoResetEvent (false); ? static EventWaitHandle go = new AutoResetEvent (false); ? static volatile string task; ? ? static void Main() { ??? new Thread (Work).Start(); ? ??? // 給工作線程發(fā)5次信號(hào) ??? for (int i = 1; i <= 5; i++) { ????? ready.WaitOne();??????????????? // 首先等待,直到工作線程準(zhǔn)備好了 ????? task = "a".PadRight (i, 'h');?? // 給任務(wù)賦值 ????? go.Set();?????????????????????? // 告訴工作線程開始執(zhí)行! ??? } ? ??? // 告訴工作線程用一個(gè)null任務(wù)來結(jié)束 ??? ready.WaitOne(); task = null; go.Set(); ? } ? ? static void Work() { ??? while (true) { ????? ready.Set();????????????????????????? // 指明我們已經(jīng)準(zhǔn)備好了 ????? go.WaitOne();???????????????????????? // 等待被踢脫... ????? if (task == null) return;???????????? // 優(yōu)雅地退出 ????? Console.WriteLine (task); ??? } ? } }ah
ahh
ahhh
ahhhh
注意我們要給task賦null來告訴工作線程退出。在工作線程上調(diào)用Interrupt?或Abort?效果是一樣的,倘若我們先調(diào)用ready.WaitOne的話。因?yàn)樵谡{(diào)用ready.WaitOne后我們就知道工作線程的確切位置,不是在就是剛剛在go.WaitOne語句之前,因此避免了中斷任意代碼的復(fù)雜性。調(diào)用?Interrupt?或?Abort需要我們?cè)诠ぷ骶€程中捕捉異常。
生產(chǎn)者/消費(fèi)者隊(duì)列
另一個(gè)普遍的線程方案是在后臺(tái)工作進(jìn)程從隊(duì)列中分配任務(wù)。這叫做生產(chǎn)者/消費(fèi)者隊(duì)列:在工作線程中生產(chǎn)者入列任務(wù),消費(fèi)者出列任務(wù)。這和上個(gè)例子很像,除了當(dāng)工作線程正忙于一個(gè)任務(wù)時(shí)調(diào)用者沒有被阻止之外。
生產(chǎn)者/消費(fèi)者隊(duì)列是可縮放的,因?yàn)槎鄠€(gè)消費(fèi)者可能被創(chuàng)建——每個(gè)都服務(wù)于相同的隊(duì)列,但開啟了一個(gè)分離的線程。這是一個(gè)很好的方式利用多處理器的系統(tǒng)來限制工作線程的數(shù)量一直避免了極大的并發(fā)線程的缺陷(過多的內(nèi)容切換和資源連接)。
在下面例子里,一個(gè)單獨(dú)的AutoResetEvent被用于通知工作線程,它只有在用完任務(wù)時(shí)(隊(duì)列為空)等待。一個(gè)通用的集合類被用于隊(duì)列,必須通過鎖控制它的訪問以確保線程安全。工作線程在隊(duì)列為null任務(wù)時(shí)結(jié)束:
using System; using System.Threading; using System.Collections.Generic; ? class ProducerConsumerQueue : IDisposable { ? EventWaitHandle wh = new AutoResetEvent (false); ? Thread worker; ? object locker = new object(); ? Queue<string> tasks = new Queue<string>(); ? ? public ProducerConsumerQueue() { ??? worker = new Thread (Work); ??? worker.Start(); ? } ? ? public void EnqueueTask (string task) { ??? lock (locker) tasks.Enqueue (task); ??? wh.Set(); ? } ? ? public void Dispose() { ??? EnqueueTask (null); ????// 告訴消費(fèi)者退出 ??? worker.Join();????????? // 等待消費(fèi)者線程完成 ??? wh.Close();???????????? // 釋放任何OS資源 ? } ? ? void Work() { ??? while (true) { ????? string task = null; ????? lock (locker) ??? ????if (tasks.Count > 0) { ????????? task = tasks.Dequeue(); ????????? if (task == null) return; ??????? } ????? if (task != null) { ??????? Console.WriteLine ("Performing task: " + task); ??????? Thread.Sleep (1000);? // 模擬工作... ????? } ????? else ??????? wh.WaitOne();???????? // 沒有任務(wù)了——等待信號(hào) ??? } ? } }下面是一個(gè)主方法測(cè)試這個(gè)隊(duì)列:
class Test { ? static void Main() { ??? using (ProducerConsumerQueue q = new ProducerConsumerQueue()) { ????? q.EnqueueTask ("Hello"); ????? for (int i = 0; i < 10; i++) q.EnqueueTask ("Say " + i); ????? q.EnqueueTask ("Goodbye!"); ??? } ??? // 使用using語句的調(diào)用q的Dispose方法, ??? // 它入列一個(gè)null任務(wù),并等待消費(fèi)者完成 ? } }Performing task: Hello
Performing task: Say 1
Performing task: Say 2
Performing task: Say 3
...
...
Performing task: Say 9
Goodbye!
注意我們明確的關(guān)閉了Wait Handle在ProducerConsumerQueue被銷毀的時(shí)候,因?yàn)樵诔绦虻纳芷谥形覀兛赡軡撛诘貏?chuàng)建和銷毀許多這個(gè)類的實(shí)例。
ManualResetEvent
ManualResetEvent是AutoResetEvent變化的一種形式,它的不同之處在于:在線程被WaitOne的調(diào)用而通過的時(shí)候,它不會(huì)自動(dòng)地reset,這個(gè)過程就像大門一樣——調(diào)用Set打開門,允許任何數(shù)量的已執(zhí)行WaitOne的線程通過;調(diào)用Reset關(guān)閉大門,可能會(huì)引起一系列的“等待者”直到下次門打開。
你可以用一個(gè)布爾字段"gateOpen" (用?volatile?關(guān)鍵字來聲明)與"spin-sleeping" –?方式結(jié)合——重復(fù)地檢查標(biāo)志,然后讓線程休眠一段時(shí)間的方式,來模擬這個(gè)過程。
ManualResetEvent有時(shí)被用于給一個(gè)完成的操作發(fā)送信號(hào),又或者一個(gè)已初始化正準(zhǔn)備執(zhí)行工作的線程。
互斥(Mutex)
Mutex提供了與C#的lock語句同樣的功能,這使它大多時(shí)候變得的冗余了。它的優(yōu)勢(shì)在于它可以跨進(jìn)程工作——提供了一計(jì)算機(jī)范圍的鎖而勝于程序范圍的鎖。
Mutex是相當(dāng)快的,而lock?又要比它快上數(shù)百倍,獲取Mutex需要花費(fèi)幾微秒,獲取lock需花費(fèi)數(shù)十納秒(假定沒有阻止)。
對(duì)于一個(gè)Mutex類,WaitOne獲取互斥鎖,當(dāng)被搶占后時(shí)發(fā)生阻止。互斥鎖在執(zhí)行了ReleaseMutex之后被釋放,就像C#的lock語句一樣,Mutex只能從獲取互斥鎖的這個(gè)線程上被釋放。
Mutex在跨進(jìn)程的普遍用處是確保在同一時(shí)刻只有一個(gè)程序的的實(shí)例在運(yùn)行,下面演示如何使用:
class OneAtATimePlease { ? // 使用一個(gè)應(yīng)用程序的唯一的名稱(比如包括你公司的URL) ? static Mutex mutex = new Mutex (false, "oreilly.com OneAtATimeDemo"); ? ??static void Main() { ??? //等待5秒如果存在競(jìng)爭(zhēng)——存在程序在 ??? // 進(jìn)程中的的另一個(gè)實(shí)例關(guān)閉之后 ? ??? if (!mutex.WaitOne (TimeSpan.FromSeconds (5), false)) { ????? Console.WriteLine ("Another instance of the app is running. Bye!"); ????? return; ??? } ??? try { ????? Console.WriteLine ("Running - press Enter to exit"); ????? Console.ReadLine(); ??? } ??? finally { mutex.ReleaseMutex(); } ? } }Mutex有個(gè)好的特性是,如果程序結(jié)束時(shí)而互斥鎖沒通過ReleaseMutex首先被釋放,CLR將自動(dòng)地釋放Mutex。
Semaphore
Semaphore就像一個(gè)夜總會(huì):它有固定的容量,這由保鏢來保證,一旦它滿了就沒有任何人可以再進(jìn)入這個(gè)夜總會(huì),并且在其外會(huì)形成一個(gè)隊(duì)列。然后,當(dāng)人一個(gè)人離開時(shí),隊(duì)列頭的人便可以進(jìn)入了。構(gòu)造器需要至少兩個(gè)參數(shù)——夜總會(huì)的活動(dòng)的空間,和夜總會(huì)的容量。
Semaphore?的特性與Mutex?和?lock有點(diǎn)類似,除了Semaphore沒有“所有者”——它是不可知線程的,任何在Semaphore內(nèi)的線程都可以調(diào)用Release,而Mutex?和?lock僅有那些獲取了資源的線程才可以釋放它。
在下面的例子中,10個(gè)線程執(zhí)行一個(gè)循環(huán),在中間使用Sleep語句。Semaphore確保每次只有不超過3個(gè)線程可以執(zhí)行Sleep語句:
class SemaphoreTest { ? static Semaphore s = new Semaphore (3, 3);? // Available=3; Capacity=3 ? ? static void Main() { ??? for (int i = 0; i < 10; i++) new Thread (Go).Start(); ? } ? ? static void Go() { ??? while (true) { ????? s.WaitOne(); ????? Thread.Sleep (100);?? // 每次只有3個(gè)線程可以到達(dá)這里 ????? s.Release(); ??? } ? } }WaitAny, WaitAll 和 SignalAndWait
除了Set?和?WaitOne方法外,在類WaitHandle中還有一些用來創(chuàng)建復(fù)雜的同步過程的靜態(tài)方法。
WaitAny,?WaitAll?和?SignalAndWait使跨多個(gè)可能為不同類型的等待句柄變得容易。
SignalAndWait可能是最有用的了:他在某個(gè)WaitHandle上調(diào)用WaitOne,并在另一個(gè)WaitHandle上自動(dòng)地調(diào)用Set。你可以在一對(duì)EventWaitHandle上裝配兩個(gè)線程,而讓它們?cè)谀硞€(gè)時(shí)間點(diǎn)“相遇”,這馬馬虎虎地合乎規(guī)范。AutoResetEvent?或?ManualResetEvent都無法使用這個(gè)技巧。第一個(gè)線程像這樣:
WaitHandle.SignalAndWait (wh1, wh2);同時(shí)第二個(gè)線程做相反的事情:
WaitHandle.SignalAndWait (wh2, wh1);WaitHandle.WaitAny等待一組等待句柄任意一個(gè)發(fā)出信號(hào),WaitHandle.WaitAll等待所有給定的句柄發(fā)出信號(hào)。與票據(jù)旋轉(zhuǎn)門的例子類似,這些方法可能同時(shí)地等待所有的旋轉(zhuǎn)門——通過在第一個(gè)打開的時(shí)候(WaitAny情況下),或者等待直到它們所有的都打開(WaitAll情況下)。
WaitAll?實(shí)際上是不確定的值,因?yàn)檫@與單元模式線程——從COM體系遺留下來的問題,有著奇怪的聯(lián)系。WaitAll?要求調(diào)用者是一個(gè)多線程單元——?jiǎng)偳墒菃卧J阶钸m合——尤其是在 Windows Forms程序中,需要執(zhí)行任務(wù)像與剪切板結(jié)合一樣庸俗!
幸運(yùn)地是,在等待句柄難使用或不適合的時(shí)候,.NET framework提供了更先進(jìn)的信號(hào)結(jié)構(gòu)——Monitor.Wait?和?Monitor.Pulse。
同步環(huán)境
與手工的鎖定相比,你可以進(jìn)行說明性的鎖定,用衍生自ContextBoundObject?并標(biāo)以Synchronization特性的類,它告訴CLR自動(dòng)執(zhí)行鎖操作,看這個(gè)例子:
using System; using System.Threading; using System.Runtime.Remoting.Contexts; ? [Synchronization] public class AutoLock : ContextBoundObject { ? public void Demo() { ??? Console.Write ("Start..."); ??? Thread.Sleep (1000);?????????? // 我們不能搶占到這 ??? Console.WriteLine ("end");???? // 感謝自動(dòng)鎖! ? } } ? public class Test { ? public static void Main() { ??? AutoLock safeInstance = new AutoLock(); ??? new Thread (safeInstance.Demo).Start();???? // 并發(fā)地 ??? new Thread (safeInstance.Demo).Start();???? // 調(diào)用Demo ??? safeInstance.Demo();??????????????????????? // 方法3次 ? } }Start... end
Start... end
Start... end
CLR確保了同一時(shí)刻只有一個(gè)線程可以執(zhí)行?safeInstance中的代碼。它創(chuàng)建了一個(gè)同步對(duì)象來完成工作,并在每次調(diào)用safeInstance的方法和屬性時(shí)在其周圍只能夠行鎖定。鎖的作用域——這里是safeInstance對(duì)象,被稱為同步環(huán)境。
那么,它是如何工作的呢?Synchronization特性的命名空間:System.Runtime.Remoting.Contexts是一個(gè)線索。ContextBoundObject可以被認(rèn)為是一個(gè)“遠(yuǎn)程”對(duì)象,這意味著所有方法的調(diào)用是被監(jiān)聽的。讓這個(gè)監(jiān)聽稱為可能,就像我們的例子AutoLock,CLR自動(dòng)的返回了一個(gè)具有相同方法和屬性的AutoLock對(duì)象的代理對(duì)象,它扮演著一個(gè)中間者的角色。總的來說,監(jiān)聽在每個(gè)方法調(diào)用時(shí)增加了數(shù)微秒的時(shí)間。
自動(dòng)同步不能用于靜態(tài)類型的成員,和非繼承自?ContextBoundObject(例如:Windows?Form)的類。
鎖在內(nèi)部以相同的方式運(yùn)作,你可能期待下面的例子與之前的有一樣的結(jié)果:
[Synchronization] public class AutoLock : ContextBoundObject { ? public void Demo() { ??? Console.Write ("Start..."); ??? Thread.Sleep (1000); ??? Console.WriteLine ("end"); ? } ? ? public void Test() { ??? new Thread (Demo).Start(); ??? new Thread (Demo).Start(); ??? new Thread (Demo).Start(); ??? Console.ReadLine(); ? } ? ? public static void Main() { ??? new AutoLock().Test(); ? } }(注意我們放入了Console.ReadLine語句。)因?yàn)樵谕粫r(shí)刻的同一個(gè)此類的對(duì)象中只有一個(gè)線程可以執(zhí)行代碼,三個(gè)新線程將保持被阻止在Demo?放中,直到Test?方法完成,需要等待ReadLine來完成。因此我們以與之前的有相同結(jié)果而告終,但是只有在按完Enter鍵之后。這是一個(gè)線程安全的手段,差不多足夠能在類中排除任何有用的多線程!
此外,我們?nèi)晕唇鉀Q之前描述的一個(gè)問題:如果AutoLock是一個(gè)集合類,比如說,我們?nèi)匀恍枰粋€(gè)像下面一樣的鎖,假設(shè)運(yùn)行在另一個(gè)類里:
if (safeInstance.Count > 0) safeInstance.RemoveAt (0);除非使用這代碼的類本身是一個(gè)同步的ContextBoundObject!
同步環(huán)境可以擴(kuò)展到超過一個(gè)單獨(dú)對(duì)象的區(qū)域。默認(rèn)地,如果一個(gè)同步對(duì)象被實(shí)例化從在另一段代碼之內(nèi),它們擁有共享相同的同步環(huán)境(換言之,一個(gè)大鎖!)。這個(gè)行為可以由改變Synchronization特性的構(gòu)造器的參數(shù)來指定。使用SynchronizationAttribute類定義的常量之一:
| 常量 | 含義 |
| NOT_SUPPORTED | 相當(dāng)于不使用同步特性 |
| SUPPORTED | 如果從另一個(gè)同步對(duì)象被實(shí)例化,則合并已存在的同步環(huán)境,否則只剩下非同步。 |
| REQUIRED | 如果從另一個(gè)同步對(duì)象被實(shí)例化,則合并已存在的同步環(huán)境,否則創(chuàng)建一個(gè)新的同步環(huán)境。 |
| REQUIRES_NEW | 總是創(chuàng)建新的同步環(huán)境 |
所以如果SynchronizedA的實(shí)例被實(shí)例化于SynchronizedB的對(duì)象中,如果SynchronizedB像下面這樣聲明的話,它們將有分離的同步環(huán)境:
[Synchronization (SynchronizationAttribute.REQUIRES_NEW)] public class SynchronizedB : ContextBoundObject { ...越大的同步環(huán)境越容易管理,但是減少機(jī)會(huì)對(duì)有用的并發(fā)。換個(gè)有限的角度,分離的同步環(huán)境會(huì)造成死鎖,看這個(gè)例子:
[Synchronization] public class Deadlock : ContextBoundObject { ? public DeadLock Other; ? public void Demo() { Thread.Sleep (1000); Other.Hello(); } ? void Hello()?????? { Console.WriteLine ("hello");???????? } } ? public class Test { ? static void Main() { ??? Deadlock dead1 = new Deadlock(); ??? Deadlock dead2 = new Deadlock(); ??? dead1.Other = dead2; ???dead2.Other = dead1; ??? new Thread (dead1.Demo).Start(); ??? dead2.Demo(); ? } }因?yàn)槊總€(gè)Deadlock的實(shí)例在Test內(nèi)創(chuàng)建——一個(gè)非同步類,每個(gè)實(shí)例將有它自己的同步環(huán)境,因此,有它自己的鎖。當(dāng)它們彼此調(diào)用的時(shí)候,不會(huì)花太多時(shí)間就會(huì)死鎖(確切的說是一秒!)。如果Deadlock?和?Test是由不同開發(fā)團(tuán)隊(duì)來寫的,這個(gè)問題特別容易發(fā)生。別指望Test知道如何產(chǎn)生的錯(cuò)誤,更別指望他們來解決它了。在死鎖顯而易見的情況下,這與使用明確的鎖的方式形成鮮明的對(duì)比。
可重入性問題
線程安全方法有時(shí)候也被稱為可重入式的,因?yàn)樵谒鼒?zhí)行的時(shí)候可以被搶占部分線路,在另外的線程調(diào)用也不會(huì)帶來壞效果。從某個(gè)意義上講,術(shù)語線程安全?和?可重入式的是同義的或者是貼義的。
不過在自動(dòng)鎖方式上,如果Synchronization的參數(shù)可重入式的?為true的話,可重入性會(huì)有潛在的問題:
[Synchronization(true)]同 步環(huán)境的鎖在執(zhí)行離開上下文時(shí)被臨時(shí)地釋放。在之前的例子里,這將能預(yù)防死鎖的發(fā)生;很明顯很需要這樣的功能。然而一個(gè)副作用是,在這期間,任何線程都可 以自由的調(diào)用在目標(biāo)對(duì)象(“重進(jìn)入”的同步上下文)的上任何方法,而非常復(fù)雜的多線程中試圖避免不釋放資源是排在首位的。這就是可重入性的問題。
因?yàn)閇Synchronization(true)]作用于類級(jí)別,這特性打開了對(duì)于非上下文的方法訪問,由于可重入性問題使它們混入類的調(diào)用。
雖然可重入性是危險(xiǎn)的,但有些時(shí)候它是不錯(cuò)的選擇。比如:設(shè)想一個(gè)在其內(nèi)部實(shí)現(xiàn)多線程同步的類,將邏輯工作線程運(yùn)行在不同的語境中。在沒有可重入性問題的情況下,工作線程在它們彼此之間或目標(biāo)對(duì)象之間可能被無理地阻礙。
這凸顯了自動(dòng)同步的一個(gè)基本弱點(diǎn):超過適用的大范圍的鎖定帶來了其它情況沒有帶來的巨大麻煩。這些困難:死鎖,可重入性問題和被閹割的并發(fā),使另一個(gè)更簡(jiǎn)單的方案——手動(dòng)的鎖定變得更為合適。
轉(zhuǎn)載于:https://www.cnblogs.com/81/archive/2011/12/13/2286316.html
總結(jié)
以上是生活随笔為你收集整理的【转载】dotnet 线程同步的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 条款12:复制对象时勿忘其每一个部分
- 下一篇: 徽州行--歙县