c#互斥鎖Mutex類用法介紹

什麼是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實現瞭一個接口,又繼承瞭一個父類。看看它的父類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

由於這兩個函數不等效於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或者空字符串,那麼這等同於創建一個未命名的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需要註意的兩個細節

  • 可能你已經註意到瞭,例子中在給Mutex命名的字符串裡給出瞭一個“Global\”的前綴。這是因為在運行終端服務(或者遠程桌面)的服務器上,已命名的全局 mutex 有兩種可見性。如果名稱以前綴“Global\”開頭,則 mutex 在所有終端服務器會話中均為可見。如果名稱以前綴“Local\”開頭,則 mutex 僅在創建它的終端服務器會話中可見,在這種情況下,服務器上各個其他終端服務器會話中都可以擁有一個名稱相同的獨立 mutex。如果創建已命名 mutex 時不指定前綴,則它將采用前綴“Local\”。在終端服務器會話中,隻是名稱前綴不同的兩個 mutex 是獨立的 mutex,這兩個 mutex 對於終端服務器會話中的所有進程均為可見。即:前綴名稱“Global\”和“Local\”僅用來說明 mutex 名稱相對於終端服務器會話(而並非相對於進程)的范圍。最後需要註意“Global\”和“Local\”是大小寫敏感的。
  • 既然父類實現瞭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#互斥鎖Mutex類用法介紹的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: