C#線程同步(3)- 互斥量 Mutex

來源:互聯網
上載者:User

標籤:object   習慣   sea   無法   狀態   hand   會同   initial   不負責任   

什麼是Mutex

  “mutex”是術語“互斥(mutually exclusive)”的簡寫形式,也就是互斥量。互斥量跟臨界區中提到的Monitor很相似,只有擁有互斥對象的線程才具有訪問資源的許可權,由於互斥對象只有一個,因此就決定了任何情況下此共用資源都不會同時被多個線程所訪問。當前佔據資源的線程在任務處理完後應將擁有的互斥對象交出,以便其他線程在獲得後得以訪問資源。互斥量比臨界區複雜,因為使用互斥不僅僅能夠在同一應用程式不同線程中實現資源的安全共用,而且可以在不同應用程式的線程之間實現對資源的安全共用。.Net中mutex由Mutex類來表示。

先繞一小段路

  在開始弄明白Mutex如何使用之前,我們要繞一小段路再回來。

  讀書的時候,大家接觸互斥量、訊號量這些玩意兒應該是在《作業系統》這一科。所以,其實這些玩意兒出現的原由是作為OS功能而存在。來看看Mutex的聲明:

[ComVisibleAttribute(true)]  public sealed class Mutex : WaitHandle

  • 類上有個屬性:ComVisibleAttribute(true),表明該類成員對COM成員公開。不去管它,只要知道這玩意兒跟COM有關係了,那大概跟Windows關係比較密了;
  • Mutex它有個父類:WaitHandle

  於是我們不得不再走遠一些,看看WaitHandel的聲明:

[ComVisibleAttribute(true)] public abstract class WaitHandle : MarshalByRefObject, IDisposable

  WaitHandle實現了一個介面,又繼承了一個父類。IDisposable在C#線程同步(2)- 臨界區&Monitor關於Using的題外話中已簡單提到,這裡就不再多說了。看看它的父類MarshalByRefObject:

MarshalByRefObject 類 允許在支援遠端的應用程式中跨應用程式定義域邊界訪問對象。

……

備忘: 應用程式定義域是一個作業系統進程中一個或多個應用程式所駐留的分區。同一應用程式定義域中的對象直接通訊。不同應用程式定義域中的對象的通訊方式有兩種:一種是跨應用程式定義域邊界傳輸對象副本,一種是使用代理交換訊息。

MarshalByRefObject 是通過使用代理交換訊息來跨應用程式定義域邊界進行通訊的對象的基類。……

  好啦,剩下的內容不用再看,否則就繞得太遠了。我們現在知道Mutex是WaitHandle的子類(偷偷地告訴你,以後要提到的EventWaitHandle、訊號量Semaphore也是,而AutoResetEvent和ManualResetEvent則是它的孫子),而WaitHandle又繼承自具有在作業系統中跨越應用程式定義域邊界能力的MarshalByRefObject類。所以我們現在可以得到一些結論:

  • Mutex是封裝了Win32 API的類,它將比較直接地叫用作業系統“對應”部分功能;而Monitor並沒有繼承自任何父類,相對來說是.Net自己“原生”的(當然.Net最終還是要靠運行時叫用作業系統的各種API)。相較於Monitor,你可以把Mutex近似看作是一個關於Win32互斥量API的殼子。
  • Mutex是可以跨應用程式/應用程式定義域,因此可以被用於應用程式定義域/應用程式間的通訊和互斥;Monitor就我們到目前為止所見,只能在應用程式內部的線程之間通訊。其實,如果用於鎖的對象派生自MarshalByRefObject,Monitor 也可在多個應用程式定義域中提供鎖定。
  • Mutex由於需要叫用作業系統資源,因此執行的開銷比Monitor大得多,所以如果僅僅需要在應用程式內部的線程間同步操作,Monitor/lock應當是首選。

有點象Monitor?不如當它是lock。

  好了,終於繞回來了。來看看怎麼使用Mutex。

  • WaitOne() / WaitOne(Int32, Boolean) / WaitOne(TimeSpan, Boolean):請求所有權,該調用會一直阻塞到當前 mutex 收到訊號,或直至達到可選的逾時間隔。這幾個方法除了不需要提供鎖定對象作為參數外,看起來與Monitor上的Wait()方法及其重載很相似相似。不過千萬不要誤會,WaitOne()本質上跟Monitor.Enter()/TryEnter()等效,而不是Monitor.Wait()!這是因為這個WaitOne()並沒有辦法在擷取控制權以後象Monitor.Wait()釋放當前Mutex,然後阻塞自己。
  • ReleaseMutex():釋放當前 Mutex 一次。注意,這裡強調了一次,因為擁有互斥體的線程可以在重複的調用Wait系列函數而不會阻止其執行;這個跟Monitor的Enter()/Exit()可以在擷取對象鎖後可以被重複調用一樣。Mutex被調用的次數由公用語言運行庫(CLR)儲存,每WaitOne()一次計數+1,每ReleaseMutex()一次計數-1,只要這個計數不為0,其它Mutex的等待者就會認為這個Mutex沒有被釋放,也就沒有辦法獲得該Mutex。 另外,跟Monitor.Exit()一樣,只有Mutex的擁有者才能RleaseMutex(),否則會引發異常。
  • 如果線程在擁有互斥體時終止,我們稱此互斥體被遺棄(Abandoned)。在MSDN裡,微軟以警告的方式指出這屬於“嚴重的”編程錯誤。這是說擁有mutex的擁有者在獲得所有權後,WaitOne()和RelaseMutex()的次數不對等,調用者自身又不負責任地中止,造成mutex 正在保護的資源可能會處於不一致的狀態。其實,這無非就是提醒你記得在try/finally結構中使用Mutex

  回想我們在《C#線程同步(2)- 臨界區&Monitor》中提到的關於生產者和消費者的情境,由於這兩個函數不等效於Monitor的Wait()和Pulse(),所以僅靠這ReleaseMutex()和WaitOne()兩個方法Mutex還無法適用於我們那個例子。

  當然Mutext上還“算有”其它一些用於同步通知的方法,但它們都是其父類WaitHandle上的靜態方法。因此它們並不是為Mutex特意“度身訂做”的,與Mutex使用的方式有些不搭調(你可以嘗試下用Mutex替換Monitor實現我們之前的情境看看),或者說Mutex其實是有些不情願的擁有這些方法。我們會在下一篇關於EventWaitHandle的Blog中再深入一些地討論Mutex和通知的問題。這裡暫且讓我們放一放,直接借用MSDN上的樣本來簡單說明Mutex的最簡單的應用情境吧:

// This example shows how a Mutex is used to synchronize access // to a protected resource. Unlike Monitor, Mutex can be used with // WaitHandle.WaitAll and WaitAny, and can be passed across // AppDomain boundaries.
using System; using System.Threading;
class Test {     

    // Create a new Mutex. The creating thread does not own the     // Mutex.    

    private static Mutex mut = new Mutex();     

    private const int numIterations = 1;     

    private const int numThreads = 3;
    static void Main()    

    {         

    // Create the threads that will use the protected resource.        

    for(int i = 0; i < numThreads; i++)        

    {            

         Thread myThread = new Thread(new ThreadStart(MyThreadProc));  

          myThread.Name = String.Format("Thread{0}", i + 1);            

          myThread.Start();     

    }
        // The main thread exits, but the application continues to         

       // run until all foreground threads have exited.    

}
    private static void MyThreadProc()    

{        

 for(int i = 0; i < numIterations; i++)  

  {            

  UseResource();   

   }   

}
    // This method represents a resource that must be synchronized     

    // so that only one thread at a time can enter.    

  private static void UseResource()  

   {         

        // Wait until it is safe to enter.        

         mut.WaitOne();
        Console.WriteLine("{0} has entered the protected area",       

       Thread.CurrentThread.Name);
        // Place code to access non-reentrant resources here.
        // Simulate some work.        

  Thread.Sleep(500);
        Console.WriteLine("{0} is leaving the protected area\r\n",

         Thread.CurrentThread.Name);

          // Release the Mutex.

          mut.ReleaseMutex();

    }

}

  雖然這隻是一個示意性的執行個體,但是我仍然不得不因為這個樣本中沒有使用try/finally來保證ReleaseMutex的執行而表示對微軟的鄙視。對於一個初學的人來說,第一個看到的例子可能會永遠影響這個人使用的習慣,所以是否在簡單示意的同時,也能“簡單地”給大家show一段足夠規範的代碼?更何況有相當部分的人都是直接copy sample code……一邊告誡所有人Abandoned Mutexes的危害,一邊又給出一段一個異常就可以輕易引發這種錯誤的sample,MSDN不可細看。

  我不得不說Mutex的作用於其說象Monitor不如說象lock,因為它只有等效於Monitro.Enter()/Exit()的作用,不同之處在於Mutex請求的鎖就是它自己。正因為如此,Mutex是可以也是必須(否則哪來的鎖?)被執行個體化的,而不象Monitor是個Static類,不能有自己的執行個體。

全域和局部的Mutex

  如果在一個應用程式定義域內使用Mutex,當然不如直接使用Monitor/lock更為合適,因為前面已經提到Mutex需要更大的開銷而執行較慢。不過Mutex畢竟不是Monitor/lock,它生來應用的情境就應該是用於進程間同步的。

  除了在上面範例程式碼中沒有參數的建構函式外,Mutex還可以被其它的建構函式所建立:

  • Mutex():用無參數的建構函式得到的Mutex沒有任何名稱,而進程間無法通過變數的形式共用資料,所以沒有名稱的Mutex也叫做局部(Local)Mutex。另外,這樣建立出的Mutex,建立者對這個執行個體並沒有擁有權,仍然需要調用WaitOne()去請求所有權。
  • Mutex(Boolean initiallyOwned):與上面的建構函式一樣,它只能建立沒有名稱的局部Mutex,無法用於進程間的同步。Boolean參數用於指定在建立者建立Mutex後,是否立刻獲得擁有權,因此Mutex(false)等效於Mutex()。
  • Mutex(Boolean initiallyOwned, String name):在這個建構函式裡我們除了能指定是否在建立後獲得初始擁有權外,還可以為這個Mutex取一個名字。只有這種命名的Mutex才可以被其它應用程式定義域中的程式所使用,因此這種Mutex也叫做全域(Global)Mutex。如果String為null或者Null 字元串,那麼這等同於建立一個未命名的Mutex。因為可能有其他程式先於你建立了同名的Mutex,因此返回的Mutex執行個體可能只是指向了同名的Mutex而已。但是,這個建構函式並沒有任何機制告訴我們這個情況。因此,如果要建立一個命名的Mutex,並且期望知道這個Mutex是否由你建立,最好使用下面兩個建構函式中的任意一個。最後,請注意name是大小寫敏感的。
  • Mutex(Boolean initiallyOwned, String name, out Boolean createdNew):頭兩個參數與上面的建構函式相同,第三個out參數用於表明是否獲得了初始的擁有權。這個建構函式應該是我們在實際中使用較多的。
  • Mutex(Boolean initiallyOwned, String name, out Booldan createdNew, MutexSecurity):多出來的這個MutexSecurity參數,也是由於全域Mutex的特性所決定的。因為可以在作業系統範圍內被訪問,因此它引發了關於訪問權的安全問題,比如哪個Windows賬戶啟動並執行程式可以訪問這個Mutex,是否可以修改這個Mutext等等。關於Mutex安全性的問題,這裡並不打算仔細介紹了,看看這裡應該很容易明白。

  另外,Mutex還有兩個重載的OpenExisting()方法可以開啟已經存在的Mutex。

Mutex的用途

  如前所述,Mutex並不適合於有相互訊息通知的同步;另一方面而我們也多次提到局部Mutex應該被Monitor/lock所取代;而跨應用程式的、相互訊息通知的同步由將在後面講到的EventWaiteHandle/AutoResetEvent/ManualResetEvent承擔更合適。所以,Mutex在.net中應用的情境似乎不多。不過,Mutex有個最常見的用途:用於控制一個應用程式只能有一個執行個體運行。

using System; using System.Threading;
class MutexSample

{    

    private static Mutex mutex = null;  //設為Static成員,是為了在整個程式生命週期內持有Mutex

    static void Main()   

    {        

       bool firstInstance;   

        mutex = new Mutex(true, @"Global\MutexSampleApp", out firstInstance);   

         try       

         {             

    if (!firstInstance)  

               {               

          Console.WriteLine ("已有執行個體運行,輸入斷行符號退出……");

                    Console.ReadLine();               

           return;

               }

              else  

             {

                   Console.WriteLine ("我們是第一個執行個體!");   

                  for (int i=60; i > 0; --i)                

                  {

                       Console.WriteLine (i);

                       Thread.Sleep(1000);

                  }

            }

        }

        finally  

       {            

           //只有第一個執行個體獲得控制權,因此只有在這種情況下才需要ReleaseMutex,否則會引發異常。

            if (firstInstance)   

            {                

       mutex.ReleaseMutex();

            }            

        mutex.Close();

                  mutex = null;

        }

    }

}

  這是一個控制台程式,你可以在編譯後嘗試一次運行多個程式,結果當然總是只有一個程式在倒數計時。你可能會在互連網上找到其它實現應用程式單例的方法,比如利用 Process 尋找進程名、利用Win32 API findwindow 尋找表單的方式等等,不過這些方法都不能保證絕對的單例。因為多進程和多線程是一樣的,由於CPU時間片隨機分配的原因,可能出現多個進程同時檢查到沒有其它執行個體啟動並執行狀況。這點在CPU比較繁忙的情況下容易出現,現實的例子比如傲遊瀏覽器。即便你設定了只允許一個執行個體運行,當系統比較忙的時候,只要你嘗試多次開啟瀏覽器,那就有可能“幸運”的開啟若干獨立的瀏覽器視窗。

  別忘了,要實現應用程式的單例,需要在在整個應用程式運行過程中都保持Mutex,而不只是在程式初始階段。所以,例子中Mutex的建立和銷毀程式碼封裝裹了整個Main()函數。

使用Mutex需要注意的兩個細節

  1. 可能你已經注意到了,例子中在給Mutex命名的字串裡給出了一個“Global\”的首碼。這是因為在運行終端服務(或者遠端桌面)的伺服器上,已命名的全域 mutex 有兩種可見度。如果名稱以首碼“Global\”開頭,則 mutex 在所有終端伺服器會話中均為可見。如果名稱以首碼“Local\”開頭,則 mutex 僅在建立它的終端伺服器會話中可見,在這種情況下,伺服器上各個其他終端伺服器會話中都可以擁有一個名稱相同的獨立 mutex。如果建立已命名 mutex 時不指定首碼,則它將採用首碼“Local\”。在終端伺服器會話中,只是名稱首碼不同的兩個 mutex 是獨立的 mutex,這兩個 mutex 對於終端伺服器會話中的所有進程均為可見。即:首碼名稱“Global\”和“Local\”僅用來說明 mutex 名稱相對於終端伺服器會話(而並非相對於進程)的範圍。最後需要注意“Global\”和“Local\”是大小寫敏感的。
  2. 既然父類實現了IDisposalble介面,那麼說明這個類一定需要你手工釋放那些非託管的資源。所以必須使用try/finally,亦或我討厭的using,調用Close()方法來釋放Mutex所佔用的所有資源!

題外話:   

很奇怪,Mutex的父類WaitHandle實現了IDisposable,但是我們在Mutex上卻找不到Dispose()方法,由於這個原因上面代碼的finally中我們用的是Close()來釋放Mutex所佔用的資源。其實,這裡的Close()就等效於Dispose(),可這是為什嗎?   再去看看WaitHandle,我們發現它實現的Disopose()方法是protected的,因此我們沒有辦法直接調用它。而它公開了一個Close()方法給調用者們用於替代Dispose(),因此Mutex上也就只有Close()。可這又是為什嗎?   話說.Net最初的設計師是微軟從Borland公司挖過來的,也就是Delphi之父。熟悉Delphi的人都知道,Object Pascal構架中用於釋放資源的方法就是Dispose(),所以Dispose()也成為.Net構架中的重要的一員。   不過從語義上來講,對於檔案、網路連接之類的資源“Close”比“Dispose”更符合我們的習慣。因此“體貼”的微軟為了讓使用者(也就是我們這些寫代碼的人)更“舒服”,在這種語義上更適合用Close的資源上,總是提供Close()作為Disopose()的公用實現。其實Close()內部不過是直接調用Dispose()而已。對於這種做法,我在感動之餘實在覺得有些多餘了,到底要把一個東西搞得多麼千變萬化才肯罷休?   如果你實在喜歡Dispose(),那麼可以用向上轉型 ((IDisposable)((WaitHandle)mutex)).Dispose()把它找出來。即強制把mutex轉換為WaitHandle,然後再把WaitHandle強制轉型為IDisposable,而IDisposable上的Dispose()是public的。不過我們終究並不確定Mutex以及WaitHandle的Close()中到底是不是在override的時候加入了什麼邏輯,所以還是老老實實用Close()好了~

C#線程同步(3)- 互斥量 Mutex

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.