C#线程同步(3)- 互斥量 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实现了一个接口,又继承了一个父类。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 ",

         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需要注意的两个细节

  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()好了~

原文地址:https://www.cnblogs.com/qi123/p/9340726.html