一、前言
互斥锁用于保护临界资源,本文是在对linux中的互斥锁有了一定理解之后再对C#中的互斥锁进行分析,互斥锁的作用以及linux中的互斥锁请看我这篇博客https://www.cnblogs.com/Suzkfly/p/14363619.html
本文是在查阅了一些网上的资料,以及自己对官方的Mutex类和WaitHandle类的理解的情况下写出的,由于本人也是初学,可能会有不正确的情况,还请指正。
互斥锁的几个基本操作:初始化锁、上锁、解锁、销毁锁,但在C#里好像不需要销毁锁(没查到相关资料)。
互斥锁在多线程里使用才有意义,关于多线程的用法,参阅我写的这篇文章:https://www.cnblogs.com/Suzkfly/p/15840584.html
二、引出需求
先看一段代码:
using System; using System.Threading; namespace mutex_test { class Program { public static int a = 0; public static void test() { while (true) { a = 3; Console.WriteLine("test a = {0}", a); Thread.Sleep(1000); //模拟复杂的计算过程 a++; Console.WriteLine("test a = {0}", a); } } static void Main(string[] args) { Thread thread = new Thread(new ThreadStart(test)); thread.Start(); while (true) { a = 1; Console.WriteLine("Main a = {0}", a); Thread.Sleep(1000); //模拟复杂的计算过程 a++; Console.WriteLine("Main a = {0}", a); } } } }
这个程序在Main方法以及test方法中都使用到了变量a,我们希望,在Main方法中开始让a=1,然后经过一段时间让a加1,那么a的值就是2,所以在Main方法中,我们希望a的值是1,2,1,2...这样交替的,同理在test方法中我们希望a的值是3,4,3,4...交替,但是运行结果如下:
从结果看出,Main线程在遇到第一个Sleep时线程就睡眠了,这时就转到test线程中去执行,a的值变为了3,test线程遇到Sleep也睡眠了,1S过后Main线程苏醒了,此时它想让a自加,但是此时a的值已经被test线程变为了3,所以Main线程中a自加之后a的值就变为了4,这就是上述程序运行的结果,这个结果并不是我们想要的。a这个变量就是临界资源,我们希望在一个线程在使用临界资源时,不会被别的线程打断,这时就可以使用互斥锁。
三、简单用法
使用互斥锁需要引用的命名空间同多线程一样,都是System.Threading。
互斥锁的类名为“Mutex”,转到Mutex的定义如下图:
而Mutex又是继承自WaitHandle:
再往上的父类就不探究了。互斥锁由易到难一点点的说。
首先可以这样创建一个互斥锁:
Mutex mutex = new Mutex();
这样去获得锁的权限:
mutex.WaitOne();
这样去释放锁:
mutex.ReleaseMutex();
将上一节的代码稍微改一下,变成下面这个样子:
using System; using System.Threading; namespace mutex_test { class Program { public static int a = 0; public static Mutex mutex = new Mutex(); public static void test() { while (true) { mutex.WaitOne(); a = 3; Console.WriteLine("test a = {0}", a); Thread.Sleep(1000); //模拟复杂的计算过程 a++; Console.WriteLine("test a = {0}", a); mutex.ReleaseMutex(); } } static void Main(string[] args) { Thread thread = new Thread(new ThreadStart(test)); thread.Start(); while (true) { mutex.WaitOne(); a = 1; Console.WriteLine("Main a = {0}", a); Thread.Sleep(1000); //模拟复杂的计算过程 a++; Console.WriteLine("Main a = {0}", a); mutex.ReleaseMutex(); } } } }
运行结果如下:
这样才是我们想要的结果。
Mutex(bool initiallyOwned);
这个构造方法用于指示调用线程是否应具有互斥体的初始所有权,如果给调用线程赋予互斥体的初始所属权,则传入的参数为 true;否则为 false。也就是说,如果传入true,那么别的线程是获取不到锁的,除非本该线程调用ReleaseMutex()。下面两句代码效果是等同的:
Mutex mutex = new Mutex(false); Mutex mutex = new Mutex();
C#中的互斥锁是哪种锁
linux中有4种锁,分别为:普通锁、检错锁、嵌套锁和适应锁,其中普通锁和适应锁是一样的(我认为是一样的),那么C#中的互斥锁是哪种锁呢,首先看看它是不是普通锁,为此写出下列代码:
using System; using System.Threading; namespace mutex_test { class Program { static void Main(string[] args) { Mutex mutex = new Mutex(); bool ret; while (true) { ret = mutex.WaitOne(); Console.WriteLine("ret = {0}", ret); Thread.Sleep(1000); } } } }
WaitOne()这个方法是去获取锁资源,如果获取成功,那么返回true,否则用不返回,在while循环里只调用WaitOne()去获得锁资源,而不释放,如果是普通锁,那么程序将打印一次“ret = true”,而实际运行结果如下:
程序一直能打印出“ret = true”,这意味着该线程能一直获得锁,那么就能够排除它是普通锁的可能。
接下来这段代码能验证它是不是嵌套锁:
using System; using System.Threading; namespace mutex_test { class Program { public static Mutex mutex = new Mutex(true); //主线程具有初始所有权 public static void test() { while (true) { mutex.WaitOne(); Console.WriteLine("test"); Thread.Sleep(1000); mutex.ReleaseMutex(); } } static void Main(string[] args) { Thread thread = new Thread(new ThreadStart(test)); thread.Start(); for (int i = 0; i < 3; i++) { mutex.WaitOne(); Console.WriteLine("Main"); Thread.Sleep(1000); } for (int i = 0; i < 4; i++) { mutex.ReleaseMutex(); //由于线程具有初始所有权,所以这里应该多释放一次 Console.WriteLine("Main ReleaseMutex"); Thread.Sleep(1000); } while (true) { Thread.Sleep(1000); } } } }
代码分析:
Main方法中获取了3次锁资源而不释放,之后释放4次锁资源,如果这个锁是嵌套锁,那么等4次锁资源都释放完毕之后,test线程才能够获得锁资源,运行结果如下:
这个结果说明,确实是在Main线程释放了4次锁资源之后,test线程才获得了锁的所有权,说明C#中用Mutex构造出来的锁对应的是linux中的嵌套锁。
Mutex(bool initiallyOwned, string name);
这个构造方法用于给互斥锁命名,如果传入null则构造出的互斥锁也是未命名的。之前用Mutex();或者Mutex(bool initiallyOwned);构造出来的互斥锁都是没有名字的。既然有了命名的功能,那么如果在不同的线程中构造出相同名字的互斥锁会怎么样呢?请看下面的代码:
using System; using System.Threading; namespace mutex_test { class Program { public static void test() { bool ret; Mutex mutex = new Mutex(true, "MyMutex"); while (true) { ret = mutex.WaitOne(); Console.WriteLine("test ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } static void Main(string[] args) { bool ret; Mutex mutex = new Mutex(true, "MyMutex"); Thread thread = new Thread(new ThreadStart(test)); thread.Start(); while (true) { ret = mutex.WaitOne(); Console.WriteLine("Main ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } } }
代码分析:
在Main方法和test方法中都构造一个名为“MyMutex”的互斥锁,并且都给初始权限,在各自的while循环中都去获得锁的权限,然后释放,因为之前验证过,C#中的互斥锁是嵌套锁,所以在线程已经拥有锁权限的时候仍然可以用WaitOne()去获得权限,如果两个线程构造出来的锁是不同的锁,那么两个线程都可以打印出各自的ret值,运行结果如下:
这说明,test线程其实并没有获得锁的所有权,如果把代码第25行中的true改为false,那么两个线程才能够交替打印ret的值。说明,如果使用Mutex(bool initiallyOwned, string name);方法去构造一个互斥锁,并且如果已经具有相同名字的互斥锁存在,那么无论构造时传入的initiallyOwned是true还是false,该线程都不具备互斥锁的所有权,但它仍然可以使用该互斥锁。
Mutex(bool initiallyOwned, string name, out bool createdNew);
为了知道自己构造出来的互斥锁是不是已经存在,可以再传入createdNew参数,如果锁存在,那么createdNew的变为false,否则为true。代码如下:
using System; using System.Threading; namespace mutex_test { class Program { public static void test() { bool ret; bool is_new; Mutex mutex = new Mutex(true, "MyMutex", out is_new); Console.WriteLine("test is_new = {0}", is_new); while (true) { ret = mutex.WaitOne(); Console.WriteLine("test ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } static void Main(string[] args) { bool ret; bool is_new; Mutex mutex = new Mutex(false, "MyMutex", out is_new); Console.WriteLine("Main is_new = {0}", is_new); Thread thread = new Thread(new ThreadStart(test)); thread.Start(); Thread.Sleep(1000); while (true) { ret = mutex.WaitOne(); Console.WriteLine("Main ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } } }
运行结果:
该程序说明,test线程试图构造一个名为“MyMutex”的互斥锁时,发现这把锁已经存在了,所以is_new的值被赋成了false。
Mutex(bool initiallyOwned, string name, out bool createdNew, MutexSecurity mutexSecurity);
这个构造方法多了一个mutexSecurity,从名字上能看出,它与锁的安全性有关,MutexSecurity类我没有接触过,但是在网上找到一篇具有参考价值的文章:https://bbs.csdn.net/topics/280051957,我把他的代码稍微改了一下,并结合互斥锁的程序得到下面的代码:
using System; using System.Threading; using System.Security.AccessControl; using System.Security.Principal; namespace mutex_test { class Program { public static void test() { bool ret; bool is_new; Mutex mutex = new Mutex(true, "MyMutex", out is_new); Console.WriteLine("test is_new = {0}", is_new); while (true) { ret = mutex.WaitOne(); Console.WriteLine("test ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } static void Main(string[] args) { SecurityIdentifier security_identifier = new SecurityIdentifier(WellKnownSidType.NullSid, null); //只是将WorldSid改成了NullSid MutexAccessRule rule = new MutexAccessRule(security_identifier, MutexRights.FullControl, AccessControlType.Allow); MutexSecurity mutexSecurity = new MutexSecurity(); mutexSecurity.AddAccessRule(rule); bool ret; bool is_new; Mutex mutex = new Mutex(false, "MyMutex", out is_new, mutexSecurity); Console.WriteLine("Main is_new = {0}", is_new); Thread thread = new Thread(new ThreadStart(test)); thread.Start(); while (true) { ret = mutex.WaitOne(); Console.WriteLine("Main ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } } }
这份代码在程序第14行会抛出一个异常,异常类型为:System.UnauthorizedAccessException,这说明“命名互斥体存在且具有访问控制安全性,但用户不具备 System.Security.AccessControl.MutexRights.FullControl”,除非将第28行的NullSid改为WorldSid,这时不会抛出异常,但test线程得到的锁并不是一把新锁。这个例子说明,可以通过传入mutexSecurity参数来限制其他线程(也不一定是线程)的权限。
Mutex OpenExisting(string name);
这个方法的作用在官方注释里写的是“打开指定的已命名的互斥体”,实际上它是用来得到一个已经存在的Mutex对象,由于它是静态方法,因此不需要通过对象去调用。测试代码如下:
using System; using System.Threading; namespace mutex_test { class Program { public static void test() { bool ret; Mutex mutex = Mutex.OpenExisting("MyMutex"); while (true) { ret = mutex.WaitOne(); Console.WriteLine("test ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } static void Main(string[] args) { bool ret; bool is_new; Mutex mutex = new Mutex(true, "MyMutex", out is_new); Console.WriteLine("Main is_new = {0}", is_new); Thread thread = new Thread(new ThreadStart(test)); thread.Start(); while (true) { ret = mutex.WaitOne(); Console.WriteLine("Main ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } } }
在第11行去得到一个已经存在的互斥锁对象,如果指定名字的互斥锁是已经存在的,那么它与下面这一句效果是一样的:
Mutex mutex = new Mutex(true, "MyMutex", out is_new);
但是如果指定名字的互斥锁不存在,那么调用Mutex OpenExisting(string name);方法时会抛出异常。
bool TryOpenExisting(string name, out Mutex result);
这个方法试图去得到一个已经存在的互斥锁对象,如果成功,那么返回true,并且result被赋值为该互斥锁,否则返回false,不会抛出异常。测试程序如下:
using System; using System.Threading; namespace mutex_test { class Program { public static void test() { bool ret; bool success; Mutex mutex; success = Mutex.TryOpenExisting("MyMutex", out mutex); Console.WriteLine("success = {0}", success); if (success) { while (true) { ret = mutex.WaitOne(); Console.WriteLine("test ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } } static void Main(string[] args) { bool ret; bool is_new; Mutex mutex = new Mutex(true, "MyMutex", out is_new); Console.WriteLine("Main is_new = {0}", is_new); Thread thread = new Thread(new ThreadStart(test)); thread.Start(); while (true) { ret = mutex.WaitOne(); Console.WriteLine("Main ret = {0}", ret); Thread.Sleep(1000); mutex.ReleaseMutex(); } } } }
最后Mutex类里还有几个方法,研究了一下没研究出来,暂时先放放。