久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

c# 線程同步系列(一)lock與Monitor的用法

 程序積累 2014-10-21

 

lock(x)
{
  DoSomething();
}

這等效于:

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
  DoSomething();
}
finally
{
  System.Threading.Monitor.Exit(obj);
}

臨界區(qū)&Lock


一個(gè)機(jī)會(huì),,索性把線程同步的問(wèn)題在C#里面的東西都粗略看了下,。

  第一印象,,C#關(guān)于線程同步的東西好多,,保持了C#一貫的大雜燴和四不象風(fēng)格(Java/Delphi)。臨界區(qū)跟Java差不多只不過(guò)關(guān)鍵字用lock替代了synchronized,,然后又用Moniter的Wait/Pulse取代了Object的Wait/Notify,,另外又搞出來(lái)幾個(gè)Event……讓人甚是不明了。不管那么多,,一個(gè)一個(gè)來(lái)吧,。

臨界區(qū)(Critical Section)

  是一段在同一時(shí)候只被一個(gè)線程進(jìn)入/執(zhí)行的代碼。為啥要有這個(gè)東西,?

  1. 是因?yàn)檫@段代碼訪問(wèn)了“臨界資源”,,而這種資源只能同時(shí)被互斥地訪問(wèn)。舉個(gè)例子來(lái)說(shuō),,你的銀行賬戶就是一個(gè)互斥資源,,一個(gè)銀行系統(tǒng)里面改變余額(存取)的操作代碼就必須用在臨界區(qū)內(nèi),。如果你的賬戶余額是$100,000(如果是真的,,那么你就不用再往下看了,,還是睡覺(jué)去吧),,假設(shè)有兩個(gè)人同時(shí)給你匯款$50,000,。有兩個(gè)線程分別執(zhí)行這兩筆匯款業(yè)務(wù),線程A在獲取了你的賬戶余額后,,在它把新余額($150000)儲(chǔ)存回?cái)?shù)據(jù)庫(kù)以前,操作系統(tǒng)把這個(gè)線程暫停轉(zhuǎn)而把CPU的時(shí)間片分給另一個(gè)線程(是的,,這太巧了),;那么線程B此時(shí)取出的賬戶余額仍然是$10000,隨后線程B幸運(yùn)的得到的CPU時(shí)間把$50000存入你的賬戶,,那么余額變成$150000,。而此后某個(gè)時(shí)候,,線程A再次得以執(zhí)行,,它也把“新”余額$150000更新到系統(tǒng)……于是你的$50000就這么憑空消失了,。(此段省去常見(jiàn)到一個(gè)示例圖,,請(qǐng)自行想象)
  2. 是因?yàn)镺S的多任務(wù)調(diào)度,其實(shí)在原因一里面已經(jīng)提到,。如果OS不支持多任務(wù)調(diào)度,,那么線程A/線程B執(zhí)行更新余額的操作總是一個(gè)接一個(gè)進(jìn)行,那么完全不會(huì)有上面的問(wèn)題了,。在多線程的世界里,,你必須隨時(shí)做好你的代碼執(zhí)行過(guò)程隨時(shí)失去控制的準(zhǔn)備;你需要好好考慮當(dāng)代碼重新執(zhí)行的時(shí)候,,是否可以繼續(xù)正確的執(zhí)行,。一句話,你的程序段在多線程的世界里,,你所寫(xiě)的方法并不是“原子性”的操作,。

Lock關(guān)鍵字

  C#提供lock關(guān)鍵字實(shí)現(xiàn)臨界區(qū),MSDN里給出的用法:

Object thisLock = new Object();
lock (thisLock)
{
   // Critical code section
}

  lock實(shí)現(xiàn)臨界區(qū)是通過(guò)“對(duì)象鎖”的方式,,注意是“對(duì)象”,,所以你只能鎖定一個(gè)引用類型而不能鎖定一個(gè)值類型。第一個(gè)執(zhí)行該代碼的線程,,成功獲取對(duì)這個(gè)對(duì)象的鎖定,,進(jìn)而進(jìn)入臨界區(qū)執(zhí)行代碼。而其它線程在進(jìn)入臨界區(qū)前也會(huì)請(qǐng)求該鎖,,如果此時(shí)第一個(gè)線程沒(méi)有退出臨界區(qū),,對(duì)該對(duì)象的鎖定并沒(méi)有解除,那么當(dāng)前線程會(huì)被阻塞,,等待對(duì)象被釋放,。

  既然如此,在使用lock時(shí),要注意不同線程是否使用同一個(gè)“鎖”作為lock的對(duì)象?,F(xiàn)在回頭來(lái)看MSDN的這段代碼似乎很容易讓人誤解,,容易讓人聯(lián)想到這段代碼是在某個(gè)方法中存在,以為thisLock是一個(gè)局部變量,,而局部變量的生命周期是在這個(gè)方法內(nèi)部,,所以當(dāng)不同線程調(diào)用這個(gè)方法的時(shí)候,他們分別請(qǐng)求了不同的局部變量作為鎖,,那么他們都可以分別進(jìn)入臨界區(qū)執(zhí)行代碼,。因此在MSDN隨后真正的示例中,thisLock實(shí)際上是一個(gè) private的類成員變量:

using System;
using System.Threading;

class Account
{
    private Object thisLock = new Object();
    int balance;

    Random r = new Random();

    public Account(int initial)
    {
        balance = initial;
    }

    int Withdraw(int amount)
    {

        // This condition will never be true unless the lock statement
        // is commented out:
        if (balance < 0)
        {
            throw new Exception("Negative Balance");
        }

        // Comment out the next line to see the effect of leaving out
        // the lock keyword:
        lock(thisLock)
        {
            if (balance >= amount)
            {
                Console.WriteLine("Balance before Withdrawal :  " + balance);
                Console.WriteLine("Amount to Withdraw        : -" + amount);
                balance = balance - amount;
                Console.WriteLine("Balance after Withdrawal  :  " + balance);
                return amount;
            }
            else
            {
                return 0; // transaction rejected
            }
        }
    }

    public void DoTransactions()
    {
        for (int i = 0; i < 100; i++)
        {
            Withdraw(r.Next(1, 100));
        }
    }
}

class Test
{
    static void Main()
    {
        Thread[] threads = new Thread[10];
        Account acc = new Account(1000);
        for (int i = 0; i < 10; i++)
        {
            Thread t = new Thread(new ThreadStart(acc.DoTransactions));
            threads[i] = t;
        }
        for (int i = 0; i < 10; i++)
        {
            threads[i].Start();
        }
    }
}

  這個(gè)例子中,,Account對(duì)象只有一個(gè),,所以臨界區(qū)所請(qǐng)求的“鎖”是唯一的,因此用類的成員變量是可以實(shí)現(xiàn)互斥意圖的,,其實(shí)用大家通常喜歡的 lock(this)也未嘗不可,,也即請(qǐng)求這個(gè)Account實(shí)例本身作為鎖。但是如果在某種情況你的類實(shí)例并不唯一或者一個(gè)類的幾個(gè)方法之間都必須要互斥,,那么就要小心了,。必須牢記一點(diǎn),所有因?yàn)橥换コ赓Y源而需要互斥的操作,,必須請(qǐng)求“同一把鎖”才有效,。

  假設(shè)這個(gè)Account類并不只有一個(gè)Withdraw方法修改balance,而是用Withdraw()來(lái)特定執(zhí)行取款操作,,另有一個(gè) Deposit()方法專門執(zhí)行存款操作,。很顯然這兩個(gè)方法必須是互斥執(zhí)行的,所以這兩個(gè)方法中所用到的鎖也必須一致,;不能一個(gè)用thisLock,,另一個(gè)重新用一個(gè)private Object thisLock1 = new Object()。再進(jìn)一步,,其實(shí)這個(gè)操作場(chǎng)景下各個(gè)互斥區(qū)存在的目的是因?yàn)橛小癇alance”這個(gè)互斥資源,,所有有關(guān)Balance的地方應(yīng)該都是互斥的(如果你不介意讀取操作讀到的是臟數(shù)據(jù)的話,當(dāng)然也可以不用),。

題外話:
  
這么看來(lái)其實(shí)用 Balance本身作為鎖也許更為符合“邏輯”,,lock住需要互斥的資源本身不是更好理解么?不過(guò)這里Balance是一個(gè)值類型,,你并不能直接對(duì)它 lock(你可能需要用到volatile關(guān)鍵字,,它能在單CPU的情況下確保只有一個(gè)線程修改一個(gè)變量)。

Lock使用的建議

  關(guān)于使用Lock微軟給出的一些建議,。你能夠在MSDN上找到這么一段話:

  通常,,應(yīng)避免鎖定 public 類型,,否則實(shí)例將超出代碼的控制范圍。常見(jiàn)的結(jié)構(gòu) lock (this),、lock (typeof (MyType)) 和 lock ("myLock") 違反此準(zhǔn)則:
  1.如果實(shí)例可以被公共訪問(wèn),,將出現(xiàn) lock (this) 問(wèn)題。
  2.如果 MyType 可以被公共訪問(wèn),,將出現(xiàn) lock (typeof (MyType)) 問(wèn)題,。
   3.由于進(jìn)程中使用同一字符串的任何其他代碼將共享同一個(gè)鎖,所以出現(xiàn) lock("myLock") 問(wèn)題,。
  4.最佳做法是定義 private 對(duì)象來(lái)鎖定, 或 private static 對(duì)象變量來(lái)保護(hù)所有實(shí)例所共有的數(shù)據(jù),。

  lock(this)的問(wèn)題我是這么理解:

  1. 處于某種原因Account在整個(gè)程序空間內(nèi)不是唯一,那么不同Account實(shí)例的相應(yīng)方法就不可能互斥,,因?yàn)樗麄冋?qǐng)求的是不同Accout實(shí)例內(nèi)部的不同的鎖,。這時(shí)候微軟示例中的private Object thisLock仍然也避免不了這個(gè)問(wèn)題,而需要使用private static Object thisLock來(lái)解決問(wèn)題,,因?yàn)閟tatic變量是所有類實(shí)例共享的,。
  2. 猜想就算Account只有一個(gè)實(shí)例,但是如果在程序內(nèi)部被多個(gè)處理不同任務(wù)的線程訪問(wèn),,那么Account實(shí)例可能會(huì)被某段代碼直接作為鎖鎖定,;這相當(dāng)于你自己鎖定了自己,,而別人在不告訴你的情況下也可以能鎖定你,。這些情況都是你在寫(xiě)Account這個(gè)類的時(shí)候并沒(méi)有辦法作出預(yù)測(cè)的,所以你的 Withdraw代碼可能被掛起,,在多線程的復(fù)雜情況下也容易造成死鎖,。不管怎樣,你寫(xiě)這段代碼的時(shí)候肯定不會(huì)期待外部的代碼跟你使用了同一把鎖吧,?這樣很危險(xiǎn),。另外,從面向?qū)ο髞?lái)說(shuō),,這等于把方法內(nèi)部的東西隱式的暴露出去,。為了實(shí)現(xiàn)互斥,專門建立不依賴系this的代碼機(jī)制總是好的,;thisLock,,專事專用,是個(gè)好習(xí)慣,。

   MyType的問(wèn)題跟lock(this)差不多理解,,不過(guò)比lock(this)更嚴(yán)重。因?yàn)長(zhǎng)ock(typeof(MyType))鎖定住的對(duì)象范圍更為廣泛,,由于一個(gè)類的所有實(shí)例都只有一個(gè)類對(duì)象(就是擁有Static成員的那個(gè)對(duì)象實(shí)例),,鎖定它就鎖定了該對(duì)象的所有實(shí)例。同時(shí) lock(typeof(MyType))是個(gè)很緩慢的過(guò)程,并且類中的其他線程,、甚至在同一個(gè)應(yīng)用程序域中運(yùn)行的其他程序都可以訪問(wèn)該類型對(duì)象,,因此,它們都有可能鎖定類對(duì)象,,完全阻止你代碼的執(zhí)行,,導(dǎo)致你自己代碼的掛起或者死鎖。

  至于lock("myLock"),,是因?yàn)樵?NET中字符串會(huì)被暫時(shí)存放,。如果兩個(gè)變量的字符串內(nèi)容相同的話,.NET會(huì)把暫存的字符串對(duì)象分配給該變量,。所以如果有兩個(gè)地方都在使用lock(“my lock”)的話,,它們實(shí)際鎖住的是同一個(gè)對(duì)象。

.NET集合類對(duì)lock的支持

  在多線程環(huán)境中,,常會(huì)碰到的互斥資源應(yīng)該就是一些容器/集合,。因此.NET在一些集合類中(比如 ArrayList,HashTable,Queue,,Stack,,包括新增的支持泛型的List)已經(jīng)提供了一個(gè)供lock使用的對(duì)象 SyncRoot。

  在.Net1.1中大多數(shù)集合類的SyncRoot屬性只有一行代碼:return this,,這樣和lock(集合的當(dāng)前實(shí)例)是一樣的,。不過(guò)ArrayList中的SyncRoot有所不同(這個(gè)并不是我反編譯的,我并沒(méi)有驗(yàn)證這個(gè)說(shuō)法):

get

  if(this._syncRoot==null)
  {
    Interlocked.CompareExchange(refthis._syncRoot,newobject(),null);
  }
  returnthis._syncRoot;
}

題外話:
  上面反編譯的 ArrayList的代碼,,引出了個(gè)Interlocked類,,即互鎖操作,用以對(duì)某個(gè)內(nèi)存位置執(zhí)行的簡(jiǎn)單原子操作,。舉例來(lái)說(shuō)在大多數(shù)計(jì)算機(jī)上,,增加變量操作不是一個(gè)原子操作,需要執(zhí)行下列步驟:

  1. 將實(shí)例變量中的值加載到寄存器中,。
  2. 增加或減少該值,。
  3. 在實(shí)例變量中存儲(chǔ)該值。

  線程可能會(huì)在執(zhí)行完前兩個(gè)步驟后被奪走CPU時(shí)間,,然后由另一個(gè)線程執(zhí)行所有三個(gè)步驟,。當(dāng)?shù)谝粋€(gè)線程重新再開(kāi)始執(zhí)行時(shí),它改寫(xiě)實(shí)例變量中的值,,造成第二個(gè)線程執(zhí)行增減操作的結(jié)果丟失,。這根我們上面提到的銀行賬戶余額的例子是一個(gè)道理,不過(guò)是更微觀上的體現(xiàn),。我們使用該類提供了的 Increment和Decrement方法就可以避免這個(gè)問(wèn)題,。
  另外,,Interlocked類上提供了其它一些能保證對(duì)相關(guān)變量的操作是原子性的方法。如Exchange()可以保證指定變量的值交換操作的原子性,,Read()保證在32位操作系統(tǒng)中對(duì)64位變量的原子讀取,。而這里使用的 CompareExchange方法組合了兩個(gè)操作:保證了比較和交換操作按原子操作執(zhí)行。此例中CompareExchange方法將當(dāng)前 syncRoot和null做比較,,如果相等,,就用new object()替換SyncRoot。
  在現(xiàn)代處理器中,,Interlocked 類的方法經(jīng)??梢杂蓡蝹€(gè)指令來(lái)實(shí)現(xiàn),因此它們的執(zhí)行性能非常高,。雖然Interlocked沒(méi)有直接提供鎖定或者發(fā)送信號(hào)的能力,,但是你可以用它編寫(xiě)鎖和信號(hào),從而編寫(xiě)出高效的非阻止并發(fā)的應(yīng)用程序,。但是這需要復(fù)雜的低級(jí)別編程能力,,因此大多數(shù)情況下使用lock或其它簡(jiǎn)單鎖是更好的選擇。

 

  看到這里是不是已經(jīng)想給微軟一耳光了,?一邊教導(dǎo)大家不要用lock(this),,一邊竟然在基礎(chǔ)類庫(kù)中大量使用……呵呵,我只能說(shuō)據(jù)傳從.Net2.0開(kāi)始SyncRoot已經(jīng)是會(huì)返回一個(gè)單獨(dú)的類了,,想來(lái)大約應(yīng)該跟ArrayList那種實(shí)現(xiàn)差不多,,有興趣的可以反編譯驗(yàn)證下。

  這里想說(shuō),,代碼是自己的寫(xiě)的,,最好減少自己代碼對(duì)外部環(huán)境的依賴,,事實(shí)證明即便是.Net基礎(chǔ)庫(kù)也不是那么可靠,。自己能想到的問(wèn)題,最好自己寫(xiě)代碼去處理,,需要鎖就自己聲明一個(gè)鎖,;不再需要一個(gè)資源那么自己代碼去Dispose掉(如果是實(shí)現(xiàn)IDisposable接口的)……不要想著什么東西系統(tǒng)已經(jīng)幫你做了。你永遠(yuǎn)無(wú)法保證你的類將會(huì)在什么環(huán)境下被使用,,你也無(wú)法預(yù)見(jiàn)到下一版的Framework是否偷偷改變了實(shí)現(xiàn),。當(dāng)你代碼莫名其妙不 Work的時(shí)候,你是很難找出由這些問(wèn)題引發(fā)的麻煩,。只有你代碼足夠的獨(dú)立(這里沒(méi)有探討代碼耦合度的問(wèn)題),,才能保證它足夠的健壯;別人代碼的修改(哪怕是你看來(lái)“不當(dāng)”的修改),,造成你的Code無(wú)法工作不是總有些可笑么(我還想說(shuō)“蒼蠅不叮無(wú)縫的蛋”“不要因?yàn)閯e人的錯(cuò)誤連累自己”),?

  一些集合類中還有一個(gè)方法是和同步相關(guān)的:Synchronized,,該方法返回一個(gè)集合的內(nèi)部類,該類是線程安全的,,因?yàn)樗拇蟛糠址椒ǘ加?lock來(lái)進(jìn)行了同步處理(你會(huì)不會(huì)想那么SyncRoot顯得多余,?別急。),。比如,,Add方法會(huì)類似于:

public override void Add(objectkey,objectvalue) 

  lock(this._table.SyncRoot)
  {
    this._table.Add(key,value);
  } 
}

  不過(guò)即便是這個(gè)Synchronized集合,在對(duì)它進(jìn)行遍歷時(shí),,仍然不是一個(gè)線程安全的過(guò)程,。當(dāng)你遍歷它時(shí),其他線程仍可以修改該它(Add,、Remove),,可能會(huì)導(dǎo)致諸如下標(biāo)越界之類的異常;就算不出錯(cuò),,你也可能讀到臟數(shù)據(jù),。若要在遍歷過(guò)程中保證線程安全,還必須在整個(gè)遍歷過(guò)程中鎖定集合,,我想這才是SynRoot存在的目的吧:

Queue myCollection = newQueue();
lock(myCollection.SyncRoot)
{
  foreach(ObjectiteminmyCollection)
  { 
    //Insert your code here.
  } 
}

  提供SynRoot是為了把這個(gè)已經(jīng)“線程安全”的集合內(nèi)部所使用的“鎖”暴露給你,,讓你和它內(nèi)部的操作使用同一把鎖,這樣才能保證在遍歷過(guò)程互斥掉其它操作,,保證你在遍歷的同時(shí)沒(méi)有可以修改,。另一個(gè)可以替代的方法,是使用集合上提供的靜態(tài)ReadOnly()方法,,來(lái)返回一個(gè)只讀的集合,,并對(duì)它進(jìn)行遍歷,這個(gè)返回的只讀集合是線程安全的,。

  到這里似乎關(guān)于集合同步的方法似乎已經(jīng)比較清楚了,,不過(guò)如果你是一個(gè)很迷信MS基礎(chǔ)類庫(kù)的人,那么這次恐怕又會(huì)失望了,。微軟決定所有從那些自 Framwork 3.0以來(lái)加入的支持泛型的集合中,,如List,取消掉創(chuàng)建同步包裝器的能力,,也就是它們不再有Synchronized,,IsSynchronized 也總會(huì)返回false;而ReadOnly這個(gè)靜態(tài)方法也變?yōu)槊麨锳sReadOnly的實(shí)例方法,。作為替代,,MS建議你仍然使用lock關(guān)鍵字來(lái)鎖定整個(gè)集合。

  至于List之類的泛型集合SyncRoot是怎樣實(shí)現(xiàn)的,,MSDN是這樣描述的“在 List<(Of <(T>)>) 的默認(rèn)實(shí)現(xiàn)中,,此屬性始終返回當(dāng)前實(shí)例,。”,,趕緊去吐血吧,!

自己的SyncRoot

還是上面提過(guò)的老話,靠自己,,以不變應(yīng)萬(wàn)變:

public class MySynchronizedList
{
  private readonly object syncRoot = new object();
  private readonly List<intlist = new List<int>();

  public object SyncRoot
  {
    get{return this.syncRoot;}
  }

  public void Add(int i)
  {
    lock(syncRoot)
    {
      list.Add(i);
    }
  }

  //...
}

自已寫(xiě)一個(gè)類,,用自己的syncRoot封裝一個(gè)線程安全的容器。

臨界區(qū)&Monitor

 

監(jiān)視器(Monitor)的概念

  可以在MSDN(http://msdn.microsoft.com/zh-cn/library/ms173179(VS.80).aspx)上找到下面一段話:

與lock關(guān)鍵字類似,,監(jiān)視器防止多個(gè)線程同時(shí)執(zhí)行代碼塊,。Enter方法允許一個(gè)且僅一個(gè)線程繼續(xù)執(zhí)行后面的語(yǔ)句;其他所有線程都將被阻止,,直到執(zhí)行語(yǔ)句的線程調(diào)用Exit,。這與使用lock關(guān)鍵字一樣。事實(shí)上,,lock 關(guān)鍵字就是用Monitor 類來(lái)實(shí)現(xiàn)的,。例如:

lock(x)
{
  DoSomething();
}

這等效于:

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
  DoSomething();
}
finally
{
  System.Threading.Monitor.Exit(obj);
}

使用 lock 關(guān)鍵字通常比直接使用 Monitor 類更可取,一方面是因?yàn)?lock 更簡(jiǎn)潔,,另一方面是因?yàn)?lock 確保了即使受保護(hù)的代碼引發(fā)異常,,也可以釋放基礎(chǔ)監(jiān)視器。這是通過(guò) finally 關(guān)鍵字來(lái)實(shí)現(xiàn)的,,無(wú)論是否引發(fā)異常它都執(zhí)行關(guān)聯(lián)的代碼塊,。

  這里微軟已經(jīng)說(shuō)得很清楚了,Lock就是用Monitor實(shí)現(xiàn)的,,兩者都是C#中對(duì)臨界區(qū)功能的實(shí)現(xiàn),。用ILDASM打開(kāi)含有以下代碼的exe 或者dll也可以證實(shí)這一點(diǎn)(我并沒(méi)有自己證實(shí)):

lock (lockobject)
{
  int i = 5;
}

反編譯后的的IL代碼為:

IL_0045:  call       void [mscorlib]System.Threading.Monitor::Enter(object)
IL_004a:  nop
.try
{
  IL_004b:  nop
  IL_004c:  ldc.i4.5
  IL_004d:  stloc.1
  IL_004e:  nop
  IL_004f:  leave.s    IL_0059
}  // end .try
finally
{
  IL_0051:  ldloc.3
  IL_0052:  call       void [mscorlib]System.Threading.Monitor::Exit(object)
  IL_0057:  nop
  IL_0058:  endfinally
}  // end handler

Monitor中和lock等效的方法

  Monitor是一個(gè)靜態(tài)類,因此不能被實(shí)例化,,只能直接調(diào)用Monitor上的各種方法來(lái)完成與lock相同的功能:

  • Enter(object)/TryEnter(object)/TryEnter(object, int32)/TryEnter(object, timespan):用來(lái)獲取對(duì)象鎖(Lock中已經(jīng)提到過(guò),,這里再?gòu)?qiáng)調(diào)一次,是對(duì)象類型而不能是值類型),,標(biāo)記臨界區(qū)的開(kāi)始,。與Enter不同,TryEnter永遠(yuǎn)不會(huì)阻塞代碼,,當(dāng)無(wú)法獲取對(duì)象鎖時(shí)它會(huì)返回False,并且調(diào)用者不進(jìn)入臨界區(qū),。TryEnter還有兩種重載,,可以定義一個(gè)時(shí)間段,在該時(shí)間段內(nèi)一直嘗試獲得對(duì)象鎖,,超時(shí)則返回False,。
  • Exit(object):沒(méi)啥好說(shuō)的,,釋放對(duì)象鎖、退出臨界區(qū),。只是一定記得在try的finally塊里調(diào)用,,否則一但由于異常造成Exit無(wú)法執(zhí)行,對(duì)象鎖得不到釋放,,就會(huì)造成死鎖,。此外,調(diào)用Exit的線程必須擁有 object 參數(shù)上的鎖,,否則會(huì)引發(fā)SynchronizationLockException異常,。在調(diào)用線程獲取指定對(duì)象上的鎖后,可以重復(fù)對(duì)該對(duì)象進(jìn)行了相同次數(shù)的 Exit 和 Enter 調(diào)用,;如果調(diào)用 Exit 與調(diào)用 Enter 的次數(shù)不匹配,,那么該鎖不會(huì)被正確釋放。

  上篇中提到的有關(guān)lock的所有使用方法和建議,,都適用于它們,。

比lock更“高級(jí)”的Monitor

  到此為止,所有見(jiàn)到的還是我們?cè)趌ock中熟悉的東西,,再看Monitor的其它方法之前,,我們來(lái)看看那老掉牙的“生產(chǎn)者和消費(fèi)者”場(chǎng)景。試想消費(fèi)者和生產(chǎn)者是兩個(gè)獨(dú)立的線程,,同時(shí)訪問(wèn)一個(gè)容器:

  • 很顯然這個(gè)容器是一個(gè)臨界資源(你不會(huì)問(wèn)我為什么是顯然吧,?),同時(shí)只允許一個(gè)線程訪問(wèn),。
  • 生產(chǎn)者往容器里存放生產(chǎn)好的資源,;消費(fèi)者消費(fèi)掉容器里的資源。

  粗看這個(gè)場(chǎng)景并沒(méi)有什么特殊的問(wèn)題,,只要在兩個(gè)線程中分別調(diào)用兩個(gè)方法,,這兩個(gè)方法內(nèi)部都用同一把鎖進(jìn)入臨界區(qū)訪問(wèn)容器即可??墒菃?wèn)題在于:

  • 消費(fèi)者鎖定容器,,進(jìn)入臨界區(qū)后可能發(fā)現(xiàn)容器是空的。它可以退出臨界區(qū),,然后下次再盲目地進(jìn)入碰碰運(yùn)氣,;如果不退出,那么讓生產(chǎn)者永遠(yuǎn)無(wú)法進(jìn)入臨界區(qū),,往容器里放入資源供消費(fèi)者消費(fèi),,從而造成死鎖。
  • 而生產(chǎn)者也可能進(jìn)入臨界區(qū)后,,卻發(fā)現(xiàn)容器是滿的,。結(jié)果一樣,,直接退出等下次來(lái)碰運(yùn)氣;或者不退出造成死鎖,。

  兩者選擇直接退出不會(huì)引發(fā)什么問(wèn)題,,無(wú)非就是可能多次無(wú)功而返。這么做,,你的程序邏輯總是有機(jī)會(huì)得到正確執(zhí)行的,,但是效率很低,因?yàn)檫@樣的機(jī)制本身是不可控的,,業(yè)務(wù)邏輯是否得以成功執(zhí)行完全是隨機(jī)的,。

  所以我們需要更有效、更“優(yōu)雅”的方式:

  • 消費(fèi)者在進(jìn)入臨界區(qū)發(fā)現(xiàn)容器為空后,,立即釋放鎖并把自己阻塞,,等待生產(chǎn)者通知,不再做無(wú)謂的嘗試,;如果順利消費(fèi)資源完畢后,,主動(dòng)通知生產(chǎn)者可以進(jìn)行生產(chǎn)了,隨后仍然阻塞自己等待生產(chǎn)者通知,。
  • 生產(chǎn)者如果發(fā)現(xiàn)容器是滿的,,那么立即釋放鎖并阻塞自己,等待消費(fèi)者在消費(fèi)完成后喚醒,;在生產(chǎn)完畢后,,主動(dòng)給消費(fèi)者發(fā)出通知,隨后也仍然阻塞自己,,等待消費(fèi)者告訴自己容器已經(jīng)空了,。

  在按這個(gè)思路寫(xiě)出Sample Code前,我們來(lái)看Monitor上需要用的其它重要方法:

  • Wait(Object)/Wait(Object, Int32)/Wait(Object, TimeSpan)/Wait(Object, Int32, Boolean)/Wait(Object, TimeSpan, Boolean):  釋放對(duì)象上的鎖并阻塞當(dāng)前線程,,直到它重新獲取該鎖,。
    1. 這里的阻塞是指當(dāng)前線程進(jìn)入“WaitSleepJoin”狀態(tài),此時(shí)CPU不再會(huì)分配給這種狀態(tài)的線程CPU時(shí)間片,,這其實(shí)跟在線程上調(diào)用 Sleep()時(shí)的狀態(tài)一樣,。這時(shí),線程不會(huì)參與對(duì)該鎖的分配爭(zhēng)奪,。
    2. 要打破這種狀態(tài),,需要其它擁有該對(duì)象鎖的線程,調(diào)用下面要講到的Pulse()來(lái)喚醒,。不過(guò)這與,,Sleep()不同,只有那些因?yàn)樵搶?duì)象鎖阻塞的線程才會(huì)被喚醒,。此時(shí),,線程重新進(jìn)入“Running”狀態(tài),參與對(duì)對(duì)象鎖的爭(zhēng)奪,。
    3. 強(qiáng)調(diào)一下,,Wait()其實(shí)起到了Exit()的作用,也就是釋放當(dāng)前所獲得的對(duì)象鎖,。只不過(guò)Wait()同時(shí)又阻塞了自己,。
    4. 我們還看到Wait()的幾個(gè)重載方法。其中第2,、3個(gè)方法給Wait加上了一個(gè)時(shí)間,,如果超時(shí)Wait會(huì)返回不再阻塞,并且可以根據(jù)Wait 方法的返回值,,以確定它是否已在超時(shí)前重新獲取鎖,。在這種情況下,其實(shí)線程并不需要等待其它線程Pulse()喚醒,,相當(dāng)于Sleep一定時(shí)間后醒來(lái),。第 4、5個(gè)方法在第2,、3個(gè)方法的基礎(chǔ)上加上exitContent參數(shù),,我們暫時(shí)不去管它,你可以詳細(xì)參見(jiàn)這里:http://msdn.microsoft.com/zh-cn/library/79fkfcw1(VS.85).aspx,。
  • Pulse(object):向阻塞線程隊(duì)列(由于該object而轉(zhuǎn)入WaitSleepJoin狀態(tài)的所有線程,,也就是那些執(zhí)行了Wait(object)的線程,存放的隊(duì)列)中第一個(gè)線程發(fā)信號(hào),,該信號(hào)通知鎖定對(duì)象的狀態(tài)已更改,,并且鎖的所有者準(zhǔn)備釋放該鎖。收到信號(hào)的阻塞線程進(jìn)入就緒隊(duì)列中(那些處于Running狀態(tài)的線程,,可以被CPU調(diào)用運(yùn)行的線程在這個(gè)隊(duì)列里),,以便它有機(jī)會(huì)接收對(duì)象鎖。注意,,接受到信號(hào)的線程只會(huì)從阻塞中被喚醒,,并不一定會(huì)獲得對(duì)象鎖。
  • PulseAll(object):與Pulse()不同,,阻塞隊(duì)列中的所有線程都會(huì)收到信號(hào),,并被喚醒轉(zhuǎn)入 Running狀態(tài),即進(jìn)入就緒隊(duì)列中,。至于它們誰(shuí)會(huì)幸運(yùn)的獲得對(duì)象鎖,,那就要看CPU了。
  • 注意:以上所有方法都只能在臨界區(qū)內(nèi)被調(diào)用,換句話說(shuō),,只有對(duì)象鎖的獲得者能夠正確調(diào)用它們,,否則會(huì)引發(fā) SynchronizationLockException異常?!?/LI>

  好了,,有了它們我們就可以完成這樣的代碼:

using System;
using System.Threading;
using System.Collections;
using System.Linq;
using System.Text;

class MonitorSample
{
    //容器,一個(gè)只能容納一塊糖的糖盒子,。PS:現(xiàn)在MS已經(jīng)不推薦使用ArrayList,,
    //支持泛型的List才是應(yīng)該在程序中使用的,我這里偷懶,,不想再去寫(xiě)一個(gè)Candy類了,。
    private ArrayList _candyBox = new ArrayList(1);
    private volatile bool _shouldStop = false; //用于控制線程正常結(jié)束的標(biāo)志

    /// <summary>
    /// 用于結(jié)束Produce()和Consume()在輔助線程中的執(zhí)行
    /// </summary>
    public void StopThread()
    {
        _shouldStop = true;
        //這時(shí)候生產(chǎn)者/消費(fèi)者之一可能因?yàn)樵谧枞卸鴽](méi)有機(jī)會(huì)看到結(jié)束標(biāo)志,
        //而另一個(gè)線程順利結(jié)束,,所以剩下的那個(gè)一定長(zhǎng)眠不醒,,需要我們?cè)谶@里嘗試叫醒它們。
        //不過(guò)這并不能確保線程能順利結(jié)束,,因?yàn)榭赡芪覀儎倓偘l(fā)送信號(hào)以后,,線程才阻塞自己。
        Monitor.Enter(_candyBox);
        try
        {
            Monitor.PulseAll(_candyBox);
        }
        finally
        {
            Monitor.Exit(_candyBox);
        }
     }

    /// <summary>
    /// 生產(chǎn)者的方法
    /// </summary>
    public void Produce()
    {
        while(!_shouldStop)
        {
            Monitor.Enter(_candyBox);
            try
            {
                if (_candyBox.Count==0)
                {
                    _candyBox.Add("A candy");
                    Console.WriteLine("生產(chǎn)者:有糖吃啦,!");
                    //喚醒可能現(xiàn)在正在阻塞中的消費(fèi)者
                    Monitor.Pulse(_candyBox);
                    Console.WriteLine("生產(chǎn)者:趕快來(lái)吃?。?);
                    //調(diào)用Wait方法釋放對(duì)象上的鎖,,并使生產(chǎn)者線程狀態(tài)轉(zhuǎn)為WaitSleepJoin,,阻止該線程被CPU調(diào)用(跟Sleep一樣)
                    //直到消費(fèi)者線程調(diào)用Pulse(_candyBox)使該線程進(jìn)入到Running狀態(tài)
                    Monitor.Wait(_candyBox);
                }
                else //容器是滿的
                {
                    Console.WriteLine("生產(chǎn)者:糖罐是滿的!");
                    //喚醒可能現(xiàn)在正在阻塞中的消費(fèi)者
                    Monitor.Pulse(_candyBox);
                    //調(diào)用Wait方法釋放對(duì)象上的鎖,,并使生產(chǎn)者線程狀態(tài)轉(zhuǎn)為WaitSleepJoin,,阻止該線程被CPU調(diào)用(跟Sleep一樣)
                    //直到消費(fèi)者線程調(diào)用Pulse(_candyBox)使生產(chǎn)者線程重新進(jìn)入到Running狀態(tài),此才語(yǔ)句返回
                    Monitor.Wait(_candyBox);
                }
            }
            finally
            {
                Monitor.Exit(_candyBox);
            }
            Thread.Sleep(2000);
        }
        Console.WriteLine("生產(chǎn)者:下班啦,!");
    }

    /// <summary>
    /// 消費(fèi)者的方法
    /// </summary>
    public void Consume()
    {
         //即便看到結(jié)束標(biāo)致也應(yīng)該把容器中的所有資源處理完畢再退出,,否則容器中的資源可能就此丟失
        //不過(guò)這里_candyBox.Count是有可能讀到臟數(shù)據(jù)的,好在我們這個(gè)例子中只有兩個(gè)線程所以問(wèn)題并不突出
        //正式環(huán)境中,,應(yīng)該用更好的辦法解決這個(gè)問(wèn)題,。
        while (!_shouldStop || _candyBox.Count > 0) 
        {
            Monitor.Enter(_candyBox);
            try
            {
                if (_candyBox.Count==1)
                {
                    _candyBox.RemoveAt(0);
                    if (!_shouldStop)
                    {
                        Console.WriteLine("消費(fèi)者:糖已吃完!");
                    }
                    else
                    {
                        Console.WriteLine("消費(fèi)者:還有糖沒(méi)吃,,馬上就完,!");
                    }
                    //喚醒可能現(xiàn)在正在阻塞中的生產(chǎn)者
                    Monitor.Pulse(_candyBox);
                    Console.WriteLine("消費(fèi)者:趕快生產(chǎn)!,!");
                    Monitor.Wait(_candyBox);
                }
                else
                {
                    Console.WriteLine("消費(fèi)者:糖罐是空的,!");
                    //喚醒可能現(xiàn)在正在阻塞中的生產(chǎn)者
                    Monitor.Pulse(_candyBox);
                    Monitor.Wait(_candyBox);
                }
            }
            finally
            {
                Monitor.Exit(_candyBox);
            }
            Thread.Sleep(2000);
        }
        Console.WriteLine("消費(fèi)者:都吃光啦,,下次再吃!");
    }

    static void Main(string[] args)
    {
        MonitorSample ss = new MonitorSample();
        Thread thdProduce = new Thread(new ThreadStart(ss.Produce));
        Thread thdConsume = new Thread(new ThreadStart(ss.Consume));
        //Start threads.
        Console.WriteLine("開(kāi)始啟動(dòng)線程,,輸入回車終止生產(chǎn)者和消費(fèi)者的工作……/r /n******************************************");
        thdProduce.Start();
        Thread.Sleep(2000);  //盡量確保生產(chǎn)者先執(zhí)行
        thdConsume.Start();
        Console.ReadLine();  //通過(guò)IO阻塞主線程,,等待輔助線程演示直到收到一個(gè)回車
        ss.StopThread();  //正常且優(yōu)雅的結(jié)束生產(chǎn)者和消費(fèi)者線程
        Thread.Sleep(1000);  //等待線程結(jié)束
        while (thdProduce.ThreadState != ThreadState.Stopped)
        {
            ss.StopThread();  //線程還沒(méi)有結(jié)束有可能是因?yàn)樗旧硎亲枞模瑖L試使用StopThread()方法中的PulseAll()喚醒它,,讓他看到結(jié)束標(biāo)志
            thdProduce.Join(1000);  //等待生產(chǎn)這線程結(jié)束
        }
        while (thdConsume.ThreadState != ThreadState.Stopped)
        {
            ss.StopThread();
            thdConsume.Join(1000);  //等待消費(fèi)者線程結(jié)束
        }
        Console.WriteLine("******************************************/r/n輸入回車結(jié)束,!");
        Console.ReadLine();
    }
}

  可能的幾種輸出(不是全部可能):

開(kāi)始啟動(dòng)線程,,輸入回車終止生產(chǎn)者和消費(fèi)者的工作……
******************************************
生產(chǎn)者:有糖吃啦,!
生產(chǎn)者:趕快來(lái)吃!,!

消費(fèi)者:還有糖沒(méi)吃,,馬上就完!
消費(fèi)者:趕快生產(chǎn)??!
生產(chǎn)者:下班啦!
消費(fèi)者:都吃光啦,,下次再吃,!
******************************************
輸入回車結(jié)束!

開(kāi)始啟動(dòng)線程,,輸入回車終止生產(chǎn)者和消費(fèi)者的工作……
******************************************
生產(chǎn)者:有糖吃啦,!
生產(chǎn)者:趕快來(lái)吃!,!
消費(fèi)者:糖已吃完,!
消費(fèi)者:趕快生產(chǎn)!,!

生產(chǎn)者:下班啦,!
消費(fèi)者:都吃光啦,下次再吃,!
******************************************
輸入回車結(jié)束,!

開(kāi)始啟動(dòng)線程,輸入回車終止生產(chǎn)者和消費(fèi)者的工作……
******************************************
生產(chǎn)者:有糖吃啦,!
生產(chǎn)者:趕快來(lái)吃?。?BR>消費(fèi)者:糖已吃完,!
消費(fèi)者:趕快生產(chǎn)?。?BR>生產(chǎn)者:有糖吃啦,!
生產(chǎn)者:趕快來(lái)吃??!

消費(fèi)者:還有糖沒(méi)吃,馬上就完,!
消費(fèi)者:趕快生產(chǎn)?。?BR>生產(chǎn)者:下班啦,!
消費(fèi)者:都吃光啦,,下次再吃!
******************************************
輸入回車結(jié)束,!

  有興趣的話你還可以嘗試修改生產(chǎn)者和消費(fèi)者的啟動(dòng)順序,,嘗試下其它的結(jié)果(比如糖罐為空)。其實(shí)生產(chǎn)者和消費(fèi)者方法中那個(gè) Sleep(2000)也是為了方便手工嘗試出不同分支的執(zhí)行情況,,輸出中的空行就是我敲入回車讓線程中止的時(shí)機(jī),。

  你可能已經(jīng)發(fā)現(xiàn),除非消費(fèi)者先于生產(chǎn)者啟動(dòng),,否則我們永遠(yuǎn)不會(huì)看到消費(fèi)者說(shuō)“糖罐是空的,!”,這是因?yàn)橄M(fèi)者在吃糖以后把自己阻塞了,,直到生產(chǎn)者生產(chǎn)出糖塊后喚醒自己,。另一方面,生產(chǎn)者即便先于消費(fèi)者啟動(dòng),,在這個(gè)例子中我們也永遠(yuǎn)不會(huì)看到生產(chǎn)者說(shuō)“糖罐是滿的,!”,因?yàn)槌跏继枪逓榭涨疑a(chǎn)者在生產(chǎn)后就把自己阻塞了,。

題外話1:
  是不是覺(jué)得生產(chǎn)者判斷糖罐是滿的,、消費(fèi)者檢查出糖罐是空的分支有些多余?
  想想,,如果糖罐初始也許并不為空,,又或者消費(fèi)者先于生產(chǎn)者執(zhí)行,那么它們就會(huì)派上用場(chǎng),。這畢竟只是一個(gè)例子,,我們?cè)跊](méi)有任何限制條件下設(shè)計(jì)了這個(gè)環(huán)環(huán)相扣的簡(jiǎn)單場(chǎng)景,所以讓這兩個(gè)分支“顯得”有些多余,,但大多數(shù)真實(shí)情況并不如此,。
  在實(shí)際應(yīng)用中,生產(chǎn)者往往代表負(fù)責(zé)從某處簡(jiǎn)單接收資源的線程,,比如來(lái)自網(wǎng)絡(luò)的指令,、從服務(wù)器返回的查詢等等;而消費(fèi)者線程需要負(fù)責(zé)解析指令,、解析返回的查詢結(jié)果,,然后存儲(chǔ)到本地?cái)?shù)據(jù)庫(kù),、文件或者呈現(xiàn)給用戶等等。消費(fèi)者線程的任務(wù)往往更復(fù)雜,,執(zhí)行時(shí)間更長(zhǎng),,為了提高程序的整體執(zhí)行效率,消費(fèi)者線程往往會(huì)多于生產(chǎn)者線程,,可能3對(duì)1,,也可能5對(duì)2……
  CPU的隨機(jī)調(diào)度,可能會(huì)造成各種各樣的情況,。你基本上是無(wú)法預(yù)測(cè)一段代碼在被調(diào)用時(shí),,與之相關(guān)的外部環(huán)境是怎樣的,所以完備的處理每一個(gè)分支是必要的,。
  另一方面,,即便一個(gè)分支的情況不是我們?cè)O(shè)計(jì)中期望發(fā)生的,但是由于某種現(xiàn)在無(wú)法預(yù)見(jiàn)的錯(cuò)誤,,造成本“不可能”、“不應(yīng)該”出現(xiàn)的分支得以執(zhí)行,,那么在這個(gè)分支的代碼可以保障你的業(yè)務(wù)邏輯可以在錯(cuò)誤的異常情況下得以修正,,至少你也可以報(bào)警避免更大的錯(cuò)誤。
  所以總是建議給每個(gè)if都寫(xiě)上else分支,,這除了讓你的代碼顯得更加僅僅有條,、邏輯清晰外,還可能給你帶來(lái)額外的擴(kuò)展性和健壯性,。就像在前一篇中所提到的,,不要因?yàn)閯e人(你所寫(xiě)類的使用者)的“錯(cuò)誤”(誰(shuí)讓你給別人這個(gè)機(jī)會(huì)呢?)連累自己,!

題外話2:
  你可以用微軟的建議用 lock(_candyBox){...} 替代上面代碼中的 Monitor.Enter(_candyBox);try{...}finally{Monitor.Exit(_candyBox);},,這里我不做任何反對(duì)。不過(guò)在更多時(shí)候,,你核能會(huì)需要在finally里做更多的事情,,而不只是Exit那么簡(jiǎn)單,所以即便用了lock,,你還得自己寫(xiě) try/finally,。
  如果你的頭已經(jīng)有些暈了,那么馬上跳過(guò)這個(gè)題外話,,下面說(shuō)的跟線程同步毫無(wú)關(guān)系,。這個(gè)題外話其實(shí)想引申到 using。這個(gè)C#特有的(其它.net語(yǔ)言沒(méi)有類似語(yǔ)法)關(guān)鍵字,,它會(huì)幫你自動(dòng)調(diào)用所有實(shí)現(xiàn)了
IDisposable接口類上的Dispose()方法,。跟lock類似,,using(obj) {//do something}等效于一個(gè)如下的try/finally語(yǔ)句塊:

SS obj = new SS();
try
{
    //use obj to do something
}
finally
{
    obj.Dispose();
}

  微軟一廂情愿的希望通過(guò)using避免程序員忘記調(diào)用Dispose()去釋放該類所占用的那些資源,包括托管的和非托管的(磁盤IO,、網(wǎng)絡(luò)IO,、數(shù)據(jù)庫(kù)連接IO等等),你通常會(huì)在關(guān)于磁盤操作的類,、各種Stream,、網(wǎng)絡(luò)操作相關(guān)的類、數(shù)據(jù)庫(kù)驅(qū)動(dòng)類上找到這個(gè)方法,。Dispose()里主要是替你 Disconnet()/Close()掉這些資源,,但是這些Dispose()方法常常是由微軟之外的公司編寫(xiě)的,比如Oracle的.Net驅(qū)動(dòng),。你能確信Oracle的程序員非常了解Dispose()在.net中的重要含義么,?回頭來(lái)說(shuō),就算是微軟自己的程序員,,難道就不會(huì)犯錯(cuò)誤嗎,?跟lock中提到的SynRoot實(shí)現(xiàn)一樣,你根本不知道你所使用類的Dispose()是否是正確的,,也無(wú)法確保下一個(gè)版本的Dispose()不會(huì)悄悄的改變…… 對(duì)于這些敏感的資源,,自己老老實(shí)實(shí)去Disconnect()/Close(),再老老實(shí)實(shí)的去Dispose(),。事實(shí)上finally需要做的事情也往往不只是一個(gè)Dispose(),。
  一句話,關(guān)于using,,堅(jiān)決反對(duì),。

  就到這里吧,好累~

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購(gòu)買等信息,,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請(qǐng)點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多