日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【转载】dotnet 线程同步

發(fā)布時(shí)間:2025/3/15 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【转载】dotnet 线程同步 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

原文:

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è)資源或某段代碼。
可被用于防止一個(gè)程序的多個(gè)實(shí)例同時(shí)運(yùn)行。

中等

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
(默認(rèn))

如果從另一個(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)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。