CodeGym /课程 /C# SELF /用于同步的互斥体: Mutex

用于同步的互斥体: Mutex

C# SELF
第 56 级 , 课程 2
可用

1. 介绍

回想上一节的例子:两个线程在我们目前还很简单的应用里对共有计数器做递增,但最终值不总是和预期一致。为了保护这个计数器我们已经用过关键字 lock(或者更精确地说,用了 Monitor),它适合在单个进程内部同步。但如果你的程序不是唯一想用这个资源的呢?比如你写了某个超级服务,运行了两个实例,它们都想写同一个文件……或者控制某个设备,比如打印机端口?这时候就派上用场了老朋友互斥体(来自 mutual exclusion — “相互排斥”)。

概念

互斥体是一种同步原语,它不仅限制同一进程内线程对资源的访问,还能让不同进程在同一台机器上协调访问。把它想象成挂在会议室门口的大牌子“占用中”,员工和访客都能看到。

在 .NET 里对应的类是 System.Threading.Mutex

什么时候真的需要 Mutex

  • 当你要在不同进程间同步访问(比如两个独立的应用同时操作同一个文件)。
  • 当资源非常宝贵且不可分割,甚至进程上下文也不能共享访问权限。

如果只是在一个进程内的线程间同步,通常用 lockMonitor)。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("临界区已释放。");
            }
        }
    }
}

试试:

  1. 在两个窗口打开这个应用。
  2. 同时运行 — 第二个会等待,直到你在第一个窗口按 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/MonitorMutexSemaphore

原语 支持进程间同步? 速度 适用场景
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(这是错误和潜在资源不一致的信号)。
2
任务
C# SELF, 第 56 级, 课程 2
已锁定
用 Mutex 简单保护资源
用 Mutex 简单保护资源
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION