首頁 > 軟體

c#互斥鎖Mutex類用法介紹

2022-02-13 19:00:56

什麼是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 arearn",
            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, @"GlobalMutexSampleApp", 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類用法介紹的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com