2.3 Mutex 锁
导读
Mutex 中文为互斥,Mutex 类叫做互斥锁。它还可用于进程间同步的同步基元。Mutex 跟 lock 相似,但是 Mutex 支持多个进程,Mutex 大约比 lock 慢 20 倍。
互斥锁(Mutex),用于多线程中防止两条线程同时对一个公共资源进行读写的机制,Mutex 只能在获得锁的线程中释放锁。
Windows 操作系统中,Mutex 同步对象有两个状态:
signaled:未被任何对象拥有;
nonsignaled:被一个线程拥有;
Mutex 锁
构造函数和方法
Mutex 类其构造函数如下:
构造函数 | 说明 |
---|---|
Mutex() | 使用默认属性初始化 Mutex类的新实例。 |
Mutex(Boolean) | 使用 Boolean 值(指示调用线程是否应具有互斥体的初始所有权)初始化 Mutex 类的新实例。 |
Mutex(Boolean, String) | 使用 Boolean 值(指示调用线程是否应具有互斥体的初始所有权以及字符串是否为互斥体的名称)初始化 Mutex 类的新实例。 |
Mutex(Boolean, String, Boolean) | 使用可指示调用线程是否应具有互斥体的初始所有权以及字符串是否为互斥体的名称的 Boolean 值和当线程返回时可指示调用线程是否已赋予互斥体的初始所有权的 Boolean 值初始化 Mutex 类的新实例。 |
Mutex 对于进程同步有所帮助,例如其应用场景主要是控制系统只能运行一个此程序的实例。
Mutex 只要考虑实现进程间的同步,它会耗费比较多的资源,进程内请考虑 Monitor/lock。
Mutex 的常用方法如下:
方法 | 说明 |
---|---|
Close() | 释放由当前 WaitHandle 占用的所有资源。 |
Dispose() | 释放由 WaitHandle 类的当前实例占用的所有资源。 |
OpenExisting(String) | 打开指定的已命名的互斥体(如果已经存在)。 |
ReleaseMutex() | 释放 Mutex一次。 |
TryOpenExisting(String, Mutex) | 打开指定的已命名的互斥体(如果已经存在),并返回指示操作是否成功的值。 |
WaitOne() | 阻止当前线程,直到当前 WaitHandle 收到信号。 |
WaitOne(Int32) | 阻止当前线程,直到当前 WaitHandle 收到信号,同时使用 32 位带符号整数指定时间间隔(以毫秒为单位)。 |
WaitOne(Int32, Boolean) | 阻止当前线程,直到当前的 WaitHandle 收到信号为止,同时使用 32 位带符号整数指定时间间隔,并指定是否在等待之前退出同步域。 |
WaitOne(TimeSpan) | 阻止当前线程,直到当前实例收到信号,同时使用 TimeSpan 指定时间间隔。 |
WaitOne(TimeSpan, Boolean) | 阻止当前线程,直到当前实例收到信号为止,同时使用 TimeSpan 指定时间间隔,并指定是否在等待之前退出同步域。 |
关于 Mutex 类,我们可以先通过几个示例去了解它。
系统只能运行一个程序的实例
下面是一个示例,用于控制系统只能运行一个此程序的实例,不允许同时启动多次。
class Program
{
// 第一个程序
const string name = "www.whuanle.cn";
private static Mutex m;
static void Main(string[] args)
{
// 本程序是否是 Mutex 的拥有者
bool firstInstance;
m = new Mutex(false,name,out firstInstance);
if (!firstInstance)
{
Console.WriteLine("程序已在运行!按下回车键退出!");
Console.ReadKey();
return;
}
Console.WriteLine("程序已经启动");
Console.WriteLine("按下回车键退出运行");
Console.ReadKey();
m.ReleaseMutex();
m.Close();
return;
}
}
上面的代码中,有些地方前面没有讲,没关系,我们运行一下生成的程序先。
解释一下上面的示例
Mutex 的工作原理:
当两个或两个以上的线程同时访问共享资源时,操作系统需要一个同步机制来确保每次只有一个线程使用资源。
Mutex 是一种同步基元,Mutex 仅向一个线程授予独占访问共享资源的权限。这个权限依据就是 互斥体,当一个线程获取到互斥体后,其它线程也在试图获取互斥体时,就会被挂起(阻塞),直到第一个线程释放互斥体。
对应我们上一个代码示例中,实例化 Mutex 类的构造函数如下:
m = new Mutex(false,name,out firstInstance);
其构造函数原型如下:
public Mutex (bool initiallyOwned, string name, out bool createdNew);
前面我们提出过,Mutex 对象有两种状态,signaled 和 nonsignaled。
通过 new 来实例化 Mutex 类,会检查系统中此互斥量 name 是否已经被使用,如果没有被使用,则会创建 name 互斥量并且此线程拥有此互斥量的使用权;此时 createdNew == true
。
那么 initiallyOwned ,它的作用是是否允许线程是否能够获取到此互斥量的初始化所有权。因为我们希望只有一个程序能够在后台运行,因此我们要设置为 false。
驱动开发中关于Mutex :https://docs.microsoft.com/zh-cn/windows-hardware/drivers/kernel/introduction-to-mutex-objects
对了, Mutex 的 参数中,name 是非常有讲究的。
在运行终端服务的服务器上,命名系统 mutex 可以有两个级别的可见性。
如果其名称以前缀 "Global\" 开头,则 mutex 在所有终端服务器会话中可见。
如果其名称以前缀 "Local\" 开头,则 mutex 仅在创建它的终端服务器会话中可见。 在这种情况下,可以在服务器上的其他每个终端服务器会话中存在具有相同名称的单独 mutex。
如果在创建已命名的 mutex 时未指定前缀,则采用前缀 "Local\"。 在终端服务器会话中,两个互斥体的名称只是它们的前缀不同,它们都是对终端服务器会话中的所有进程都可见。
也就是说,前缀名称 "Global\" 和 "Local\" 描述互斥体名称相对于终端服务器会话的作用域,而不是相对于进程。
请参考:
https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex?view=netcore-3.1#methods
https://www.cnblogs.com/suntp/p/8258488.html
接替运行
这里要实现,当同时点击一个程序时,只能有一个实例A可以运行,其它实例进入等待队列,等待A运行完毕后,然后继续运行队列中的下一个实例。
我们将每个程序比作一个人,模拟一个厕所坑位,每次只能有一个人上厕所,其他人需要排队等候。
使用 WaitOne()
方法来等待别的进程释放互斥量,即模拟排队;ReleaseMutex()
方法解除对坑位的占用。
class Program
{
// 第一个程序
const string name = "www.whuanle.cn";
private static Mutex m;
static void Main(string[] args)
{
// wc 还有没有位置
bool firstInstance;
m = new Mutex(true,name,out firstInstance);
// 已经有人在上wc
if (!firstInstance)
{
// 等待运行的实例退出,此进程才能运行。
Console.WriteLine("排队等待");
m.WaitOne();
GoWC();
return;
}
GoWC();
return;
}
private static void GoWC()
{
Console.WriteLine(" 开始上wc");
Thread.Sleep(1000);
Console.WriteLine(" 开门");
Thread.Sleep(1000);
Console.WriteLine(" 关门");
Thread.Sleep(1000);
Console.WriteLine(" xxx");
Thread.Sleep(1000);
Console.WriteLine(" 开门");
Thread.Sleep(1000);
Console.WriteLine(" 离开wc");
m.ReleaseMutex();
Thread.Sleep(1000);
Console.WriteLine(" 洗手");
}
}
此时,我们使用了
m = new Mutex(true,name,out firstInstance);
一个程序结束后,要允许其它线程能够创建 Mutex 对象获取互斥量,需要将构造函数的第一个参数设置为 true。
你也可以改成 false,看看会报什么异常。
你可以使用 WaitOne(Int32)
来设置等待时间,单位是毫秒,超过这个时间就不排队了,去别的地方上厕所。
为了避免出现问题,请考虑在 finally 块中执行 m.ReleaseMutex()
。
进程同步示例
这里我们实现一个这样的场景:
父进程 Parent 启动子进程 Children ,等待子进程 Children 执行完毕,子进程退出,父进程退出。
新建一个 .NET Core 控制台项目,名称为 Children,其 Progarm 中的代码如下
using System;
using System.Threading;
namespace Children
{
class Program
{
const string name = "进程同步示例";
private static Mutex m;
static void Main(string[] args)
{
Console.WriteLine("子进程被启动...");
bool firstInstance;
// 子进程创建互斥体
m = new Mutex(true, name, out firstInstance);
// 按照我们设计的程序,创建一定是成功的
if (firstInstance)
{
Console.WriteLine("子线程执行任务");
DoWork();
Console.WriteLine("子线程任务完成");
// 释放互斥体
m.ReleaseMutex();
// 结束程序
return;
}
else
{
Console.WriteLine("莫名其妙的异常,直接退出");
}
}
private static void DoWork()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("子线程工作中");
Thread.Sleep(TimeSpan.FromSeconds(1));
}
}
}
}
然后发布或生成项目,打开程序文件位置,复制线程文件路径。 创建一个新项目,名为 Parent 的 .NET Core 控制台,其 Program 中的代码如下:
using System;
using System.Diagnostics;
using System.Threading;
namespace Parent
{
class Program
{
const string name = "进程同步示例";
private static Mutex m;
static void Main(string[] args)
{
// 晚一些再执行,我录屏要对正窗口位置
Thread.Sleep(TimeSpan.FromSeconds(3));
Console.WriteLine("父进程启动!");
new Thread(() =>
{
// 启动子进程
Process process = new Process();
process.StartInfo.UseShellExecute = true;
process.StartInfo.CreateNoWindow = false;
process.StartInfo.WorkingDirectory = @"../../../ConsoleApp9\Children\bin\Debug\netcoreapp3.1";
process.StartInfo.FileName = @"../../../ConsoleApp9\Children\bin\Debug\netcoreapp3.1\Children.exe";
process.Start();
process.WaitForExit();
}).Start();
// 子进程启动需要一点时间
Thread.Sleep(TimeSpan.FromSeconds(1));
// 获取互斥体
bool firstInstance;
m = new Mutex(true, name, out firstInstance);
// 说明子进程还在运行
if (!firstInstance)
{
// 等待子进程运行结束
Console.WriteLine("等待子进程运行结束");
m.WaitOne();
Console.WriteLine("子进程运行结束,程序将在3秒后自动退出");
m.ReleaseMutex();
Thread.Sleep(TimeSpan.FromSeconds(3));
return;
}
}
}
}
请将 Children 项目的程序文件路径,替换到 Parent 项目启动子进程的那部分字符串中。
然后启动 Parent.exe,可以观察到如下图的运行过程:
另外
构造函数中,如果为 name
指定 null
或空字符串,则将创建一个本地 Mutex 对象,只会在进程内有效。
Mutex 有些使用方法比较隐晦,可以参考 https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.-ctor?view=netcore-3.1#SystemThreading_Mutex__ctor_System_Boolean
另外打开互斥体,请参考
https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.openexisting?view=netcore-3.1
https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.tryopenexisting?view=netcore-3.1
到目前为止,我们学习了排他锁 lock、Monitor、Mutex。下一篇我们将来学习非排他锁定结构的Semaphore
和SemaphoreSlim
。