互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex))。互斥体禁止多个线程同时进入受保护的代码“临界区”。因此,在任意时刻,只有一个线程被允许进入这样的代码保护区。
任何线程在进入临界区之前,必须获取(acquire)与此区域相关联的互斥体的所有权。如果已有另一线程拥有了临界区的互斥体,其他线程就不能再进入其中。这些线程必须等待,直到当前的属主线程释放(release)该互斥体。什么时候需要使用互斥体呢?互斥体用于保护共享的易变代码,也就是,全局或静态数据。这样的数据必须通过互斥体进行保护,以防止它们在多个线程同时访问时损坏。
Mutex是一个同步基元,它与前面提到的锁最大的区别在于它支持进程间同步。
Mutex允许同一个线程多次重复访问共享区,但是对于别的线程那就必须等待,它甚至支持不同进程中的线程同步,这点更能体现他的优势,但是劣势也是显而易见的,那就是巨大的性能损耗和容易产生死锁的困扰,所以除非需要在特殊场合,否则 我们尽量少用为妙,这里并非是将Mutex的缺点说的很严重,而是建议大家在适当的场合使用更为适合的同步方式,Mutex 就好比一个重量型的工具,利用它则必须付出性能的代价。
1、Mutex线程同步
Mutex实现线程同步主要依靠以下两个方法实现:
class Program { static void Main(string[] args) { for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(Run); } Console.ReadKey(); } static int count = 0; static Mutex mutex = new Mutex(); static void Run(object obj) { //阻止当前线程(如果去掉这两行) mutex.WaitOne(); Console.WriteLine("当前数字:{0}", ++count); //释放 Mutex (如果去掉这行) mutex.ReleaseMutex(); } }
输出:
如果去掉则输出如下:
Mutex和Monitor的区别:
2、进程间同步
当给Mutex取名的时候能够实现进程同步,不取名实现线程同步。
Mutex有两种类型:未命名的局部mutex和已命名的系统mutex。
class Program { static void Main(string[] args) { //使用线程输出等待状态 Thread t1 = new Thread(ShowMyWord); t1.Start(); Run(t1); Console.Read(); } static int count = 0; static Mutex mutex = new Mutex(false, "xxoo"); static void Run(Thread t1) { //这个WaitOne方法要么返回true,要么一直不返回(不会返回false),所以没办法用if来判断 //于是,用个线程输出等待状态 mutex.WaitOne(); Console.WriteLine("终于轮到老子了! " + DateTime.Now.TimeOfDay.ToString()); //停止线程t1,不要再输出等待状态 t1.Abort(); //模拟干活十秒 Thread.Sleep(10000); Console.WriteLine("干完! " + DateTime.Now.TimeOfDay.ToString()); //释放 Mutex mutex.ReleaseMutex(); } static void ShowMyWord(object obj) { for (int i = 0; i < 10; i++) { Thread.Sleep(2000); Console.WriteLine("我的心在等待,一直在等待! " + DateTime.Now.TimeOfDay.ToString()); } } }
以上代码,将生成的.exe文件复制两份:
快速运行两个输出如下:
3、互斥体控制控制台程序仅能启动一次
由于Mutex能够用于进程间同步,因此我们可以很轻易地利用它实现控制程序只能启动一次的效果。
static void Main(string[] args) { bool IsFirstCreate; Mutex instance = new Mutex(true, "NewApplication", out IsFirstCreate); if (IsFirstCreate) //赋予了线程初始所属权,也就是首次使用互斥体 { instance.ReleaseMutex(); } else { Console.WriteLine("你已经启动了一个程序,本程序将于5秒后自动退出!"); Thread.Sleep(5000); return; } Console.WriteLine("程序启动成功!"); Console.ReadKey(); }
输出如下:
4、控制WinForm程序只能启动一次
static class Program { /// <summary> /// 应用程序的主入口点。 /// </summary> [STAThread] static void Main() { bool IsFirstRun; Mutex instance = new Mutex(true, "xxx", out IsFirstRun); if (IsFirstRun) //赋予了线程初始所属权,也就是首次使用互斥体 { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); instance.ReleaseMutex(); } else { MessageBox.Show("你已启动了一个程序,本程序将于5秒后退出", "系统提示", MessageBoxButtons.OK, MessageBoxIcon.Error); Thread.Sleep(5000); Application.Exit(); } } }
输出如下:
第3、4测试均需要在bin目录下直接启动多个.exe。
实际引用中,可能我们对共享变量的使用并不十分复杂,可能只是一些简单的操作如:自增、自减、求和、赋值、比较等。
MSDN中的解析Interlocked为多个线程共享的变量提供原子操作。
常用操作如下:
方法 | 说明 |
Add | 相加 |
CompareExchange | 比较 |
Increment | 递增 |
Decrement | 递减 |
Exchange | 赋值 |
示例:
class Program { static void Main(string[] args) { for (int i = 0; i < 20; i++) { Thread t = new Thread(Run); t.Start(); } Console.Read(); } static int Incre = 0; static int Add = 0; static int Exchange = 0; static int Decre = 21; static int CompareExchange = 0; static Mutex mutex = new Mutex(); static void Run() { //自增操作 //Console.WriteLine("当前数字:{0}", Interlocked.Increment(ref Incre)); //递减操作 //Console.WriteLine("当前数字:{0}", Interlocked.Decrement(ref Decre)); //相加 //Console.WriteLine("当前数字:{0}", Interlocked.Add(ref Add,10)); //赋值 //Console.WriteLine("当前数字:{0}", Interlocked.Exchange(ref Exchange, 5)); //比较,如果第三个参数等于CompareExchange,则将第二个参数的值,赋给第一个参数 Console.WriteLine("当前数字:{0}", Interlocked.CompareExchange(ref CompareExchange, 15,0)); } }