1. 介绍
回想上一节的例子:两个线程在我们目前还很简单的应用里对共有计数器做递增,但最终值不总是和预期一致。为了保护这个计数器我们已经用过关键字 lock(或者更精确地说,用了 Monitor),它适合在单个进程内部同步。但如果你的程序不是唯一想用这个资源的呢?比如你写了某个超级服务,运行了两个实例,它们都想写同一个文件……或者控制某个设备,比如打印机端口?这时候就派上用场了老朋友互斥体(来自 mutual exclusion — “相互排斥”)。
概念
互斥体是一种同步原语,它不仅限制同一进程内线程对资源的访问,还能让不同进程在同一台机器上协调访问。把它想象成挂在会议室门口的大牌子“占用中”,员工和访客都能看到。
在 .NET 里对应的类是 System.Threading.Mutex。
什么时候真的需要 Mutex:
- 当你要在不同进程间同步访问(比如两个独立的应用同时操作同一个文件)。
- 当资源非常宝贵且不可分割,甚至进程上下文也不能共享访问权限。
如果只是在一个进程内的线程间同步,通常用 lock(Monitor)。Mutex 更适合用于进程间同步,因为它更“重”、更慢。
Mutex 如何工作
flowchart TD
A(进程 1) --|请求|--> M(Mutex)
B(进程 2) --|请求|--> M
M --|一次只允许一个|--> R(共享资源)
A --|释放|--> M
B --|在释放后|--> M
2. 使用 Mutex 的基本语法
创建
Mutex 的创建和大多数同步类一样简单:
using System.Threading;
Mutex mutex = new Mutex();
主要方法
- WaitOne() — 尝试获取互斥体;如果没获取到,线程会被阻塞直到互斥体被别人释放。
- ReleaseMutex() — 释放互斥体,允许其他线程或进程进入临界区。
最简单的例子:进程内线程间同步
using System;
using System.Threading;
class Program
{
static Mutex mutex = new Mutex();
static void Main()
{
Thread t1 = new Thread(PrintNumbers);
Thread t2 = new Thread(PrintNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
static void PrintNumbers()
{
for (int i = 0; i < 5; i++)
{
mutex.WaitOne(); // 进入临界区
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
mutex.ReleaseMutex(); // 退出临界区
Thread.Sleep(100); // 便于观察
}
}
}
在这个例子里两个线程轮流获取控制台的访问权来打印。
3. 使用命名互斥体进行进程间同步
要进行真正的“重武器”——进程间同步——使用命名互斥体。你给它一个名字,机器上所有进程都可以通过这个名字访问它。
Mutex mutex = new Mutex(false, "MyApp_Mutex");
构造函数参数:
- 第一个参数(bool initiallyOwned)— 是否在创建后立即由当前线程持有。通常是 false。
- 第二个参数 — 互斥体的名字。使用相同名字的所有进程访问的是同一个对象,例如 "MyApp_Mutex"。
示例:两个应用共享一个 Mutex
在两个不同窗口运行同一程序来观察效果。
using System;
using System.Threading;
class Program
{
static void Main()
{
using (Mutex mutex = new Mutex(false, "MySuperUniqueMutexName"))
{
Console.WriteLine("尝试进入临界区...");
mutex.WaitOne(); // 等待,直到另一个进程释放互斥体
try
{
Console.WriteLine("临界区被本进程占用。");
Console.WriteLine("按 Enter 退出临界区。");
Console.ReadLine();
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine("临界区已释放。");
}
}
}
}
试试:
- 在两个窗口打开这个应用。
- 同时运行 — 第二个会等待,直到你在第一个窗口按 Enter。
4. 限制应用同时运行的实例数
Mutex 常用来限制同时运行的实例数。例如:“嘿,用户,下次别开两个计算器副本!”。
using System;
using System.Threading;
class Program
{
static void Main()
{
bool createdNew;
using (Mutex mutex = new Mutex(true, "CalculatorAppInstanceMutex", out createdNew))
{
if (!createdNew)
{
Console.WriteLine("应用已经在运行!");
return;
}
Console.WriteLine("应用已成功启动。按 Enter 退出。");
Console.ReadLine();
}
}
}
这个模式在桌面应用里很常见:第一个实例启动,第二个检测到已存在实例就报告并退出。官方的 Microsoft 示例在 文档 中有说明。
5. 使用 Mutex 时的常见错误
错误 №1: 忘记调用 ReleaseMutex()。
如果线程成功获取了互斥体(WaitOne()),但没有调用 ReleaseMutex()(抛了异常或忘记了),其他线程或进程将无法进入,直到“忘记”线程结束。这可能导致 deadlock(死锁)。良好做法是总是用 try-finally:
mutex.WaitOne();
try
{
// 临界区
}
finally
{
mutex.ReleaseMutex();
}
错误 №2: 调用了不正确次数的 ReleaseMutex()。
如果调用 ReleaseMutex() 的次数多于调用 WaitOne() 的次数,会抛出 ApplicationException。
错误 №3: 试图释放不是自己持有的 Mutex。
互斥体“绑定”到获取它的线程。只有持有它的线程有权调用 ReleaseMutex()。如果其他线程尝试释放,.NET 会抛出异常。
错误 №4: 在只需要 lock 的地方使用 Mutex。
Mutex 比普通的 lock 更慢,因为它可以跨进程工作并且需要系统调用。建议:如果不需要进程间同步,就用 lock。
错误 №5: 互斥体名字选得不好。
如果名字太简单(比如 "MyMutex"),可能会意外与别的程序冲突。最好用唯一名字(例如包含公司或应用名)。
6. 有用的细节
WaitOne(timeout)
可以指定等待互斥体的超时:
if (mutex.WaitOne(5000)) // 等最长 5 秒
{
try { /* ... */ }
finally { mutex.ReleaseMutex(); }
}
else
{
Console.WriteLine("5 秒内无法获取资源!");
}
Mutex.TryOpenExisting
如果想连接到已经存在的互斥体,使用静态方法:
if (Mutex.TryOpenExisting("DiaryFileWriteMutex", out Mutex existingMutex))
{
// 现在 existingMutex 是指向已存在互斥体的引用
}
不同用户间的访问控制
默认情况下命名互斥体对有创建系统对象权限的所有用户可见。如果需要更严格的控制,可以使用带 MutexSecurity 的构造函数。
比较表:lock/Monitor、Mutex、Semaphore
| 原语 | 支持进程间同步? | 速度 | 适用场景 |
|---|---|---|---|
|
否 | 很快 | 同一进程内的线程间 |
|
是 | 较慢(开销更大) | 不同进程的线程间 |
|
是(NamedSemaphore) | 与 Mutex 相当 | 需要限制并发线程数量时 |
关于 Semaphore 的更多内容会在下一节讲。
Mutex 的状态
stateDiagram-v2
[*] --> Unowned
Unowned --> Owned : WaitOne()
Owned --> Owned : WaitOne() (reentrant)
Owned --> Unowned : ReleaseMutex()
Owned --> Abandoned : 线程“挂了”,没调用 ReleaseMutex()
Abandoned --> Unowned
- Unowned — 没人拥有互斥体。
- Owned — 互斥体被某个线程持有。
- Abandoned — 线程意外结束,没调用 ReleaseMutex()。下一个拿到互斥体的人会收到 AbandonedMutexException(这是错误和潜在资源不一致的信号)。
GO TO FULL VERSION