天天看點

臨界區&Monitor

螢幕(Monitor)的概念

  

  這裡微軟已經說得很清楚了,Lock就是用Monitor實作的,兩者都是C#中對臨界區功能的實作。用ILDASM打開含有以下代碼的exe或者dll也可以證明這一點(我并沒有自己證明):

Monitor中和lock等效的方法

  Monitor是一個靜态類,是以不能被執行個體化,隻能直接調用Monitor上的各種方法來完成與lock相同的功能:

  上篇中提到的有關lock的所有使用方法和建議,都适用于它們。

<a></a>

比lock更“進階”的Monitor

  到此為止,所有見到的還是我們在lock中熟悉的東西,再看Monitor的其它方法之前,我們來看看那老掉牙的“生産者和消費者”場景。試想消費者和生産者是兩個獨立的線程,同時通路一個容器:

很顯然這個容器是一個臨界資源(你不會問我為什麼是顯然吧?),同時隻允許一個線程通路。

生産者往容器裡存放生産好的資源;消費者消費掉容器裡的資源。

  粗看這個場景并沒有什麼特殊的問題,隻要在兩個線程中分别調用兩個方法,這兩個方法内部都用同一把鎖進入臨界區通路容器即可。可是問題在于:

消費者鎖定容器,進入臨界區後可能發現容器是空的。它可以退出臨界區,然後下次再盲目地進入碰碰運氣;如果不退出,那麼讓生産者永遠無法進入臨界區,往容器裡放入資源供消費者消費,進而造成死鎖。

而生産者也可能進入臨界區後,卻發現容器是滿的。結果一樣,直接退出等下次來碰運氣;或者不退出造成死鎖。

  兩者選擇直接退出不會引發什麼問題,無非就是可能多次無功而返。這麼做,你的程式邏輯總是有機會得到正确執行的,但是效率很低,因為這樣的機制本身是不可控的,業務邏輯是否得以成功執行完全是随機的。

  是以我們需要更有效、更“優雅”的方式:

消費者在進入臨界區發現容器為空後,立即釋放鎖并把自己阻塞,等待生産者通知,不再做無謂的嘗試;如果順利消費資源完畢後,主動通知生産者可以進行生産了,随後仍然阻塞自己等待生産者通知。

生産者如果發現容器是滿的,那麼立即釋放鎖并阻塞自己,等待消費者在消費完成後喚醒;在生産完畢後,主動給消費者發出通知,随後也仍然阻塞自己,等待消費者告訴自己容器已經空了。

  在按這個思路寫出Sample Code前,我們來看Monitor上需要用的其它重要方法:

這裡的阻塞是指目前線程進入“WaitSleepJoin”狀态,此時CPU不再會配置設定給這種狀态的線程CPU時間片,這其實跟線上程上調用Sleep()時的狀态一樣。這時,線程不會參與對該鎖的配置設定争奪。

要打破這種狀态,需要其它擁有該對象鎖的線程,調用下面要講到的Pulse()來喚醒。不過這與,Sleep()不同,隻有那些因為該對象鎖阻塞的線程才會被喚醒。此時,線程重新進入“Running”狀态,參與對對象鎖的争奪。

強調一下,Wait()其實起到了Exit()的作用,也就是釋放目前所獲得的對象鎖。隻不過Wait()同時又阻塞了自己。

注意:以上所有方法都隻能在臨界區内被調用,換句話說,隻有對象鎖的獲得者能夠正确調用它們,否則會引發SynchronizationLockException異常。 

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

  有興趣的話你還可以嘗試修改生産者和消費者的啟動順序,嘗試下其它的結果(比如糖罐為空)。其實生産者和消費者方法中那個Sleep(2000)也是為了友善手工嘗試出不同分支的執行情況,輸出中的空行就是我敲入回車讓線程中止的時機。

  你可能已經發現,除非消費者先于生産者啟動,否則我們永遠不會看到消費者說“糖罐是空的!”,這是因為消費者在吃糖以後把自己阻塞了,直到生産者生産出糖塊後喚醒自己。另一方面,生産者即便先于消費者啟動,在這個例子中我們也永遠不會看到生産者說“糖罐是滿的!”,因為初始糖罐為空且生産者在生産後就把自己阻塞了。

題外話1:

  是不是覺得生産者判斷糖罐是滿的、消費者檢查出糖罐是空的分支有些多餘?

  想想,如果糖罐初始也許并不為空,又或者消費者先于生産者執行,那麼它們就會派上用場。這畢竟隻是一個例子,我們在沒有任何限制條件下設計了這個環環相扣的簡單場景,是以讓這兩個分支“顯得”有些多餘,但大多數真實情況并不如此。

  在實際應用中,生産者往往代表負責從某處簡單接收資源的線程,比如來自網絡的指令、從伺服器傳回的查詢等等;而消費者線程需要負責解析指令、解析傳回的查詢結果,然後存儲到本地資料庫、檔案或者呈現給使用者等等。消費者線程的任務往往更複雜,執行時間更長,為了提高程式的整體執行效率,消費者線程往往會多于生産者線程,可能3對1,也可能5對2……

  CPU的随機排程,可能會造成各種各樣的情況。你基本上是無法預測一段代碼在被調用時,與之相關的外部環境是怎樣的,是以完備的處理每一個分支是必要的。

  另一方面,即便一個分支的情況不是我們設計中期望發生的,但是由于某種現在無法預見的錯誤,造成本“不可能”、“不應該”出現的分支得以執行,那麼在這個分支的代碼可以保障你的業務邏輯可以在錯誤的異常情況下得以修正,至少你也可以報警避免更大的錯誤。

  是以總是建議給每個if都寫上else分支,這除了讓你的代碼顯得更加僅僅有條、邏輯清晰外,還可能給你帶來額外的擴充性和健壯性。就像在前一篇中所提到的,不要因為别人(你所寫類的使用者)的“錯誤”(誰讓你給别人這個機會呢?)連累自己!

  你可以用微軟的建議用 lock(_candyBox){...} 替代上面代碼中的Monitor.Enter(_candyBox);try{...}finally{Monitor.Exit(_candyBox);},這裡我不做任何反對。不過在更多時候,你核能會需要在finally裡做更多的事情,而不隻是Exit那麼簡單,是以即便用了lock,你還得自己寫try/finally。

  微軟一廂情願的希望通過using避免程式員忘記調用Dispose()去釋放該類所占用的那些資源,包括托管的和非托管的(磁盤IO、網絡IO、資料庫連接配接IO等等),你通常會在關于磁盤操作的類、各種Stream、網絡操作相關的類、資料庫驅動類上找到這個方法。Dispose()裡主要是替你Disconnet()/Close()掉這些資源,但是這些Dispose()方法常常是由微軟之外的公司編寫的,比如Oracle的.Net驅動。你能确信Oracle的程式員非常了解Dispose()在.net中的重要含義麼?回頭來說,就算是微軟自己的程式員,難道就不會犯錯誤嗎?跟lock中提到的SynRoot實作一樣,你根本不知道你所使用類的Dispose()是否是正确的,也無法確定下一個版本的Dispose()不會悄悄的改變……對于這些敏感的資源,自己老老實實去Disconnect()/Close(),再老老實實的去Dispose()。事實上finally需要做的事情也往往不隻是一個Dispose()。

  一句話,關于using,堅決反對。

QQ:519841366

本頁版權歸作者和部落格園所有,歡迎轉載,但未經作者同意必須保留此段聲明,

且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利