文章原始出处 http://xxinside.blogbus.com/logs/47162540.html

预备知识:C#线程同步(1)- 临界区&LockC#线程同步(2)- 临界区&Monitor

什么是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或者空字符串,那么这等同于创建一个未命名的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的更多相关文章

  1. linux系统编程:线程同步-相互排斥量(mutex)

    线程同步-相互排斥量(mutex) 线程同步 多个线程同一时候訪问共享数据时可能会冲突,于是须要实现线程同步. 一个线程冲突的演示样例 #include <stdio.h> #includ ...

  2. UNIX环境高级编程——线程同步之互斥量

    互斥量(也称为互斥锁)出自POSIX线程标准,可以用来同步同一进程中的各个线程.当然如果一个互斥量存放在多个进程共享的某个内存区中,那么还可以通过互斥量来进行进程间的同步. 互斥量,从字面上就可以知道 ...

  3. linux线程同步(1)-互斥量

    一.概述                                                   互斥量是线程同步的一种机制,用来保护多线程的共享资源.同一时刻,只允许一个线程对临界区进行 ...

  4. 经典线程同步 互斥量Mutex

    阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <秒杀多线程第五篇经典线程同步关键段CS> <秒杀多线程第六篇经典线程同步事件Event& ...

  5. (转)经典线程同步 互斥量Mutex

    阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <秒杀多线程第五篇经典线程同步关键段CS> <秒杀多线程第六篇经典线程同步事件Event& ...

  6. 多线程面试题系列(7):经典线程同步 互斥量Mutex

    前面介绍了关键段CS.事件Event在经典线程同步问题中的使用.本篇介绍用互斥量Mutex来解决这个问题. 互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问.互斥量与关键段的行为非常相似, ...

  7. 秒杀多线程第七篇 经典线程同步 互斥量Mutex

    本文转载于:http://blog.csdn.net/morewindows/article/details/7470936 前面介绍了关键段CS.事件Event在经典线程同步问题中的使用.本篇介绍用 ...

  8. 转--- 秒杀多线程第七篇 经典线程同步 互斥量Mutex

    阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇一个经典的多线程同步问题> <秒杀多线程第五篇经典线程同步关键段CS> <秒杀多线程第六篇经典线程同步事件Event& ...

  9. 线程同步方式之互斥量Mutex

    互斥量和临界区非常相似,只有拥有了互斥对象的线程才可以访问共享资源,而互斥对象只有一个,因此可以保证同一时刻有且仅有一个线程可以访问共享资源,达到线程同步的目的. 互斥量相对于临界区更为高级,可以对互 ...

  10. 线程同步 - POSIX互斥锁

    线程同步 - POSIX互斥锁 概括 本文讲解POSIX中互斥量的基本用法,从而能达到简单的线程同步.互斥量是一种特殊的变量,它有两种状态:锁定以及解锁.如果互斥量是锁定的,就有一个特定的线程持有或者 ...

随机推荐

  1. Html转义字符列表

    Symbol Code Entity Name ™ ™   €   € Space   ! !   " " " # #   $ $   % %   & & ...

  2. flask基础二

    内容有:1.配置文件处理,2.路由系统,3.视图,4.请求,5.响应,6.模板渲染,7.session,8.flash,9.中间件,10特殊装饰器 一:一个简单知识点 通过路径构成的字符串1.动态导入 ...

  3. baiduMap & MapV 简单demo

    看到 MapV 的一个demo 的底图比较好看,练练手 MapV demos:https://mapv.baidu.com/examples/ 参考的demo:https://mapv.baidu.c ...

  4. 树莓派与Linux系统之间文件传输

    最近因为要学习Python,于是把放在家里接了一年灰的树莓派又给搜出来了,刚买那会也捣鼓了好一阵子, 基本操作都学会了,但现在又忘光了,只能又从头开始搞了,首先第一个要解决的是怎么把文件从电脑传输到树 ...

  5. Java学习之--List和ArrayList

    首先明确: List是一个接口,不能被创造实例对象 ArrayList是List 接口的一个实现类,ArrayList类实现了List接口(List接口有多个实现类,例如ArrayList,Linke ...

  6. 安全相关及HttpClient

    1,Spring Security入门示例 Spring Security Annotation Configuration Example – HelloWorld 2,程序模块Get请求,获取响应 ...

  7. liunx安装py.27

    liunx安装py.27 按网站(https://blog.csdn.net/u012071918/article/details/78817344) 上的教程安装py.27 1.安装依赖的库 在终端 ...

  8. windows环境在本地配nginx

    本地搭建了前端项目,但奈何有时候需要https访问的需求,所以做了一个尝试在本地(windows环境)下配置nginx,最终的效果就是 搭建的时候,遇到两个问题: 第一个是如果要在本地搭建https, ...

  9. python类与对象-如何创建可管理的对象属性

    如何创建可管理的对象属性 问题举例 在面向对象编程中, 我们把方法看作对象的接口, 直接访问对象的属性可能是不安全的,或设计上不够灵活. 但是使用调用方法在形式上不如访问属性简洁. circle.ge ...

  10. 2019-oo-第二次总结

    这一单元是关于模拟电梯运行,考验多线程的一个单元,难度由简入入深,从多线程单部电梯,到优化,再到多线程多部电梯,难度一次次的提高. 一.多线程单部电梯(傻瓜调度) 1.设计策略 这一次我只额外的开了一 ...