总目录
前言
在C#中,Mutex(互斥锁)是一种跨进程的同步机制,用于控制多个线程或进程对共享资源的访问。与 Monitor 和 lock 不同,Mutex 可以跨越进程边界进行同步,这使得它非常适合需要跨进程同步的场景。本文将详细介绍 Mutex 的工作原理、主要方法及其使用示例。
一、Mutex 类概述
Mutex 类位于 System.Threading 命名空间中,提供了比 lock 和 Monitor 更高级别的同步机制,支持跨进程同步。它的主要功能是通过提供一个互斥锁来控制对共享资源的访问,确保同一时刻只有一个线程或进程可以持有该锁。
1. 基本锁机制
Mutex
通过WaitOne
和ReleaseMutex
方法实现锁的获取和释放。
using System.Threading;
// 创建未命名的 Mutex(仅进程内有效)
Mutex mutex = new Mutex();
// 创建命名的跨进程 Mutex(Windows 全局命名空间)
Mutex namedMutex = new Mutex(initiallyOwned: false, "Global\\MyAppMutex");
try
{
// 尝试获取锁(阻塞或超时)
if (mutex.WaitOne(timeout: TimeSpan.FromSeconds(5)))
{
// 临界区:操作共享资源
}
else
{
Console.WriteLine("获取锁超时!");
}
}
catch (AbandonedMutexException ex)
{
// 处理其他线程未释放锁的情况
Console.WriteLine("检测到被遗弃的 Mutex!");
}
finally
{
mutex.ReleaseMutex(); // 释放锁
mutex.Close(); // 显式关闭(或使用 using 块)
}
2. 核心特性
- 命名Mutex:通过名称跨进程共享(如"
Global\\MyMutex
"),常用于多进程同步。 - 未命名Mutex:仅在同一进程内有效。
- 等待和释放:提供等待锁的方法(如 WaitOne)和释放锁的方法(如 ReleaseMutex)。
二、主要方法和属性
1. 构造函数
Mutex 类提供了两个构造函数:
- 无参数构造函数:创建一个未命名Mutex。
var mutex = new Mutex();
- 带参数的构造函数:创建一个命名Mutex,并指定是否初始拥有该互斥量。
bool createdNew;
var mutex = new Mutex(initiallyOwned: true, name: "MyGlobalMutex", createdNew: out createdNew);
public Mutex(
bool initiallyOwned, // 初始是否由创建线程拥有
string name, // 命名 Mutex 的唯一标识
out bool createdNew // 输出参数,指示是否创建了新 Mutex
)
参数说明
-
initiallyOwned:若为 true,创建线程直接获得锁所有权。
-
name:
- 命名规则:支持系统级名称(如 “
Global\\MyMutex
”)或本地名称。- Windows:支持
Global\\
和Local\\
前缀。 - Linux/macOS:名称会被视为文件路径(如 /MyAppMutex),需确保路径可写权限。
- Windows:支持
- 跨进程同步:同一名称的 Mutex 在系统范围内共享。
- 特殊前缀:
Global\\
:Windows 全局命名空间(跨用户会话)。Local\\
:当前用户会话内的命名空间(默认)。
- null 或空字符串:
- 如果 name 为 null 或空字符串,则将创建一本地 Mutex 对象,只在进程内有效。
- 命名规则:支持系统级名称(如 “
-
createdNew:用于判断是否成功创建新 Mutex(避免重复初始化)。
2. WaitOne 和 ReleaseMutex 方法
- WaitOne 方法用于等待获取互斥量。如果当前没有其他线程或进程持有该互斥量,则立即返回;否则,调用线程将被阻塞,直到获得锁。
- ReleaseMutex 方法用于释放当前线程持有的互斥量。必须确保在每次成功获取互斥量后都调用此方法,否则会导致死锁。
示例:基本用法
var mutex = new Mutex();
mutex.WaitOne(); // 等待获取互斥量
try
{
// 临界区代码
Console.WriteLine("Mutex acquired.");
}
finally
{
mutex.ReleaseMutex(); // 释放互斥量
}
3. Close 方法
Close 方法用于释放与 Mutex 对象关联的所有资源。通常在不再需要互斥量时调用此方法。
示例:关闭互斥量
mutex.Close();
4. 安全性控制
通过 MutexSecurity 设置访问权限(仅 Windows 有效):
var mutexSecurity = new MutexSecurity();
mutexSecurity.AddAccessRule(
new MutexAccessRule(
"Everyone",
MutexRights.FullControl,
AccessControlType.Allow));
using (Mutex mutex = new Mutex(
initiallyOwned: false,
name: "Global\\SecuredMutex",
createdNew: out _,
mutexSecurity: mutexSecurity))
{
// 使用受权限控制的 Mutex
}
5. 检测锁状态
通过 WaitOne 的返回值判断是否成功获取锁:
if (mutex.WaitOne(0)) // 立即返回,不阻塞
{
// 成功获取锁
}
else
{
// 锁已被其他线程/进程占用
}
三、异常与错误处理
1. AbandonedMutexException
当其他线程未释放锁直接退出时,尝试获取该锁会触发此异常:
try
{
mutex.WaitOne();
}
catch (AbandonedMutexException ex)
{
// 处理被遗弃的锁(可能需要恢复数据一致性)
Console.WriteLine($"Mutex {ex.MutexName} 被异常释放!");
mutex.ReleaseMutex(); // 手动释放
}
2. 权限不足
创建全局命名(即使用Global\\
) Mutex 可能需要管理员权限:
try
{
var mutex = new Mutex(false, "Global\\AdminMutex");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("需要管理员权限!");
}
四、使用场景、注意事项与示例
1. 使用场景
- 防止重复启动:确保应用程序在同一时间只能运行一个实例。
- 跨进程同步:确保多个进程不会同时访问共享资源。
- 如 跨进程文件访问:确保多个进程写入同一文件时互斥。
- 如 共享硬件控制:协调多个进程访问同一硬件设备(如打印机)。
- 复杂同步需求:处理涉及多个线程和进程的复杂同步问题。
- 分布式系统协调:通过Mutex实现简单的分布式锁(需谨慎设计)。
2. 注意事项
-
命名规则:命名Mutex需唯一,避免冲突(如使用GUID)。
-
资源释放:
- 必须成对调用 WaitOne 和 ReleaseMutex。
- 必须调用ReleaseMutex释放锁,否则其他线程/进程无法获取。
- 使用using块或 try-finally + Close方法确保资源释放。
-
权限问题:跨进程Mutex可能需要管理员权限(尤其是Global命名空间)。
-
死锁风险:避免跨进程锁的嵌套使用,容易导致复杂死锁。
- 跨进程锁的嵌套容易导致不可预测的死锁。
- 设置合理的超时时间(如 WaitOne(5000))。
-
跨平台限制:命名Mutex在Linux/macOS上行为可能与Windows不同(如命名格式)。
-
替代方案:使用Mutex的代价是较高的性能开销(涉及内核模式切换),因此在轻量级场景中应优先选择 Monitor 或 Semaphore。
- Mutex 只需考虑实现进程间的同步,进程内请考虑 Monitor/lock。
-
性能优化:避免频繁使用跨进程Mutex,尽量缩小临界区范围。
-
何时用Mutex:需要跨进程同步或Monitor无法满足复杂需求时。
- 需要系统级锁的场景(如单实例应用、跨进程资源控制)。
- 对同步可靠性要求极高的任务(如硬件访问)。
- 简单分布式锁的原型设计。
合理使用Mutex可以有效解决复杂的同步问题,但其系统级开销需谨慎权衡。
3. 单一进程中的mutex 示例
以下是一个简单的例子,展示了如何在单一进程中使用互斥量来保护共享资源。
示例:单一进程中的互斥量
using System;
using System.Threading;
class Program
{
// 共享资源(需要线程安全访问)
private static int _sharedCounter = 0;
// 创建Mutex对象(进程内线程同步)
private static Mutex _mutex = new Mutex();
static void Main()
{
Console.WriteLine("=== 单进程多线程Mutex演示 ===");
Console.WriteLine("请输入要创建的线程数量:");
if (!int.TryParse(Console.ReadLine(), out int threadCount) || threadCount < 1)
{
Console.WriteLine("输入无效,默认使用4个线程");
threadCount = 4;
}
// 启动多个工作线程
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++)
{
threads[i] = new Thread(IncrementCounter);
threads[i].Start(i + 1); // 传递线程ID
}
// 等待所有线程完成
foreach (var thread in threads)
{
thread.Join();
}
Console.WriteLine($"\n最终结果(理论值:{threadCount * 1000}):");
Console.WriteLine($"实际值:{_sharedCounter}");
Console.WriteLine("是否出现竞争条件? " + (_sharedCounter == threadCount * 1000 ? "否" : "是"));
Console.ReadLine();
}
static void IncrementCounter(object threadId)
{
try
{
for (int i = 0; i < 1000; i++)
{
// 通过Mutex保护临界区
_mutex.WaitOne();
try
{
// 安全地操作共享资源
_sharedCounter++;
}
finally
{
_mutex.ReleaseMutex();
}
// 模拟非临界区操作(不需要锁)
Thread.Sleep(new Random().Next(1, 5));
}
Console.WriteLine($"线程 {threadId} 完成");
}
catch (Exception ex)
{
Console.WriteLine($"线程 {threadId} 错误:{ex.Message}");
}
}
}
代码解析
1. 关键组件
组件 | 说明 |
---|---|
_sharedCounter | 被多个线程共享的计数器,用于演示资源竞争问题 |
_mutex | 进程内的Mutex对象,用于控制对_sharedCounter的访问 |
IncrementCounter | 线程方法,包含需要保护的临界区代码 |
2. 执行流程
- 用户输入线程数量:创建指定数量的线程
- 每个线程执行1000次累加:
- 使用WaitOne()获取锁
- 安全修改_sharedCounter
- 使用ReleaseMutex()释放锁
- 显示最终结果:
- 理论值 = 线程数量 × 1000
- 实际值展示是否发生资源竞争
运行结果对比
场景1:使用Mutex(线程安全)
=== 单进程多线程Mutex演示 ===
请输入要创建的线程数量:
4
线程 4 完成
线程 1 完成
线程 3 完成
线程 2 完成
最终结果(理论值:4000):
实际值:4000
是否出现竞争条件? 否
场景2:注释掉Mutex代码(模拟资源竞争)
// _mutex.WaitOne();
// try
// {
_sharedCounter++;
// }
// finally
// {
// _mutex.ReleaseMutex();
// }
结果:
实际值:3876 // 每次运行结果不同
是否出现竞争条件? 是
关键知识点
1. 临界区保护模式
_mutex.WaitOne(); // 进入临界区
try
{
// 安全操作共享资源
}
finally
{
_mutex.ReleaseMutex(); // 确保总是释放锁
}
2. 为什么需要try-finally?
- 保证即使临界区代码抛出异常,锁也会被释放
- 避免死锁(其他线程永远无法获取锁)
3. 锁粒度控制
- 需要锁的操作:共享资源修改 (_sharedCounter++)
- 不需要锁的操作:非共享操作 (Thread.Sleep)
扩展实验建议
1. 观察锁竞争
在WaitOne前添加日志:
Console.WriteLine($"线程 {threadId} 等待锁...");
_mutex.WaitOne();
Console.WriteLine($"线程 {threadId} 获得锁");
2. 尝试非阻塞锁
if (_mutex.WaitOne(0)) // 立即返回是否成功
{
try { /* 操作 */ }
finally { _mutex.ReleaseMutex(); }
}
else
{
Console.WriteLine("获取锁失败,执行其他操作");
}
3. 递归获取测试
// 同一线程多次获取锁
_mutex.WaitOne();
_mutex.WaitOne(); // 能成功吗?
_sharedCounter++;
_mutex.ReleaseMutex();
_mutex.ReleaseMutex(); // 需要释放两次吗?
4. 跨进程中的mutex 示例
1. 示例:防止重复启动的应用程序
以下是一个使用 Mutex 实现 单实例控制台应用程序 的详细案例,包含错误处理、用户提示及窗口激活功能:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
class Program
{
// 用于激活已存在实例的窗口(Windows API)
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private static void Main()
{
// 定义唯一的 Mutex 名称(全局命名空间)
const string mutexName = "Global\\MySingleInstanceConsoleApp";
bool isNewInstance;
// 创建 Mutex 并检查是否已存在实例
using (Mutex mutex = new Mutex(
initiallyOwned: true, // 初始拥有权
name: mutexName, // 全局唯一名称
createdNew: out isNewInstance))
{
try
{
if (!isNewInstance)
{
// 发现已有实例运行
Console.WriteLine("程序已在运行!正在激活已有窗口...");
ActivateExistingWindow();
Thread.Sleep(2000); // 等待用户查看提示
return;
}
// 主程序逻辑
Console.Title = "单实例示例程序 (PID: " + Process.GetCurrentProcess().Id + ")";
Console.WriteLine("程序已启动,输入 'exit' 退出。");
// 模拟工作循环
string input;
do
{
Console.Write("> ");
input = Console.ReadLine();
Console.WriteLine($"处理命令: {input}");
} while (input?.ToLower() != "exit");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("错误:需要管理员权限创建全局 Mutex!");
Thread.Sleep(3000);
}
catch (AbandonedMutexException)
{
Console.WriteLine("警告:前一个实例未正常释放 Mutex!");
}
}
}
/// <summary>
/// 激活已存在的程序窗口(仅限Windows)
/// </summary>
private static void ActivateExistingWindow()
{
Process currentProcess = Process.GetCurrentProcess();
string processName = currentProcess.ProcessName;
// 查找同名进程(排除当前进程)
foreach (Process process in Process.GetProcessesByName(processName))
{
if (process.Id == currentProcess.Id) continue;
// 激活窗口
if (process.MainWindowHandle != IntPtr.Zero)
{
SetForegroundWindow(process.MainWindowHandle);
break;
}
}
}
}
单实例场景 情况下,不需要显式调用 ReleaseMutex(),因为:
- 持有锁的目的 是阻止其他实例运行,而非临时保护共享资源。
- 程序退出时,操作系统会自动释放所有内核对象(包括 Mutex),其他实例可以重新获取。
- 在单实例场景中,主线程创建Mutex时,initiallyOwned参数设置为true,意味着主线程立即拥有该Mutex
代码详解:
1. Mutex 创建与检查
using (Mutex mutex = new Mutex(
initiallyOwned: true,
name: "Global\\MySingleInstanceConsoleApp",
createdNew: out isNewInstance))
initiallyOwned: true
:创建时立即获取所有权。Global\\
前缀:确保跨用户会话的全局唯一性(需管理员权限)。- createdNew 参数:判断是否成功创建新实例。
- using 块的自动释放:
- Mutex 实现了 IDisposable,using 块结束时调用 Dispose()。
- Dispose() 会关闭内核句柄,但 不会自动调用 ReleaseMutex()。
- 然而,在单实例场景中,程序退出后操作系统会强制释放锁,因此无需手动释放。
2. 多实例处理逻辑
if (!isNewInstance)
{
Console.WriteLine("程序已在运行!正在激活已有窗口...");
ActivateExistingWindow();
Thread.Sleep(2000);
return;
}
- 发现已有实例时,提示用户并激活已有窗口。
- 当第一次运行是实例时, isNewInstance 为ture 表示 mutex 是一个新的实例
- 当再次或多次运行实例时, 通过指定的 name
Global\\MySingleInstanceConsoleApp
去创建实例时发现系统中已存在该名称的mutex 实例,因此 isNewInstance 返回 false 表示 mutex 已经存在创建的实例
- 2秒后自动退出当前实例。
3. 窗口激活功能
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private static void ActivateExistingWindow()
{
// 查找同名进程并激活其主窗口
}
-
使用 Windows API SetForegroundWindow 将已有实例窗口置顶。
-
遍历同名进程,排除自身进程后激活第一个找到的窗口。
4. 异常处理
- UnauthorizedAccessException:处理权限不足问题(如未以管理员身份运行)。
- AbandonedMutexException:处理前一个实例未正常释放锁的情况。
5. 主程序逻辑
Console.Title = "单实例示例程序 (PID: " + Process.GetCurrentProcess().Id + ")";
Console.WriteLine("程序已启动,输入 'exit' 退出。");
// 模拟工作循环...
- 显示当前进程ID便于调试。
- 实现简单的控制台命令循环。
运行效果
- 首次运行:
程序已启动,输入 'exit' 退出。
> hello
处理命令: hello
> exit
- 二次运行
程序已在运行!正在激活已有窗口...
(2秒后自动退出,并激活第一个实例窗口)
注意事项
-
管理员权限:
- 使用
Global\\
前缀需以管理员身份运行程序,否则会触发 UnauthorizedAccessException。 - 可在项目属性中勾选【启用 ClickOnce 安全设置】→【以管理员身份运行】。
- 使用
-
跨平台限制:
- SetForegroundWindow 仅适用于 Windows。
- Linux/macOS 需使用其他方式实现窗口激活(如进程通信)。
-
Mutex 名称唯一性:
- 建议使用 GUID 或唯一标识符作为名称(如
Global\\{APP-GUID}
)。
- 建议使用 GUID 或唯一标识符作为名称(如
-
防篡改保护:
- 恶意程序可通过同名 Mutex 干扰你的应用,如需更高安全性,需结合其他机制(如加密令牌)。
2. 示例:跨进程文件写入
命名Mutex可用于协调多个进程对共享资源的访问。
// 进程A
using (Mutex mutex = new Mutex(false, "Global\\MySharedMutex"))
{
if (mutex.WaitOne(5000))
{
File.AppendAllText("shared.txt", "进程A写入内容\n");
mutex.ReleaseMutex();
}
}
// 进程B
using (Mutex mutex = new Mutex(false, "Global\\MySharedMutex"))
{
if (mutex.WaitOne(5000))
{
File.AppendAllText("shared.txt", "进程B写入内容\n");
mutex.ReleaseMutex();
}
}
-
全局命名空间:
Global\\
前缀表示系统级Mutex(Windows特有),避免用户会话隔离问题。 -
安全性:确保进程有权限访问同一命名Mutex。
3. 示例:控制硬件访问
协调多个进程使用同一硬件设备(如串口):
Mutex serialPortMutex = new Mutex(false, "Global\\SerialPortCOM1");
if (serialPortMutex.WaitOne(1000))
{
using (var port = new SerialPort("COM1"))
{
port.Open();
// 发送数据...
}
serialPortMutex.ReleaseMutex();
}
4. 示例:分布式锁原型
(需谨慎设计,通常建议使用专用分布式锁服务)
// 通过共享网络位置的文件模拟分布式锁
string lockFilePath = @"\\server\shared\lock.file";
Mutex distributedMutex = new Mutex(false, "Global\\DistributedFileLock");
if (distributedMutex.WaitOne())
{
if (!File.Exists(lockFilePath))
{
File.Create(lockFilePath).Close();
// 执行独占操作...
File.Delete(lockFilePath);
}
distributedMutex.ReleaseMutex();
}
五、最佳实践
1. 使用 using 语句
为了确保互斥量资源能够正确释放,建议使用 using 语句来管理 Mutex 对象的生命周期。
using (var mutex = new Mutex())
{
mutex.WaitOne();
try
{
// 临界区代码
}
finally
{
mutex.ReleaseMutex();
}
}
2. 处理异常
确保在捕获异常时也释放互斥量,避免死锁。
try
{
mutex.WaitOne();
// 临界区代码
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
finally
{
mutex.ReleaseMutex();
}
3. 避免死锁
确保每个获取互斥量的线程或进程最终都会释放互斥量,尤其是在复杂的应用场景中。
4. 使用命名互斥量时注意安全性
命名互斥量是全局可见的,因此要注意潜在的安全性问题。确保命名互斥量的名称具有足够的唯一性,并考虑使用安全描述符来限制访问权限。
5. 最佳实战完整示例
使用 Mutex 控制多进程对共享文件的访问
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
class Program
{
// 共享文件名
private const string LogFile = "shared_log.txt";
// 全局 Mutex 名称(跨进程唯一标识)
private const string MutexName = "Global\\MyGlobalFileMutex";
static void Main(string[] args)
{
Console.Title = $"文件写入进程 (PID: {Process.GetCurrentProcess().Id})";
Console.WriteLine("=== 跨进程安全文件写入演示 ===");
Console.WriteLine("输入要模拟的写入线程数量(建议3-5):");
if (!int.TryParse(Console.ReadLine(), out int threadCount) || threadCount < 1)
{
Console.WriteLine("输入无效,默认使用3个线程");
threadCount = 3;
}
// 初始化共享文件(清空内容)
File.WriteAllText(LogFile, $"日志初始化时间: {DateTime.Now:HH:mm:ss.fff}\n\n");
// 启动多个模拟线程(每个线程代表一个独立操作)
for (int i = 0; i < threadCount; i++)
{
int threadId = i + 1;
new Thread(() => WriteToFile(threadId)).Start();
Thread.Sleep(100); // 错开线程启动时间
}
Console.WriteLine("\n按任意键退出...");
Console.ReadKey();
}
static void WriteToFile(int threadId)
{
const int maxRetry = 2;
int attempt = 0;
while (attempt <= maxRetry)
{
try
{
using (Mutex mutex = new Mutex(false, MutexName))
{
Console.WriteLine($"[线程{threadId}] 尝试获取文件锁... (尝试 {attempt + 1}/3)");
// 尝试获取锁(最多等待3秒)
if (mutex.WaitOne(TimeSpan.FromSeconds(3)))
{
try
{
// 模拟复杂操作
var content = $"进程 {Process.GetCurrentProcess().Id} - 线程 {threadId} " +
$"于 {DateTime.Now:HH:mm:ss.fff} 写入\n";
File.AppendAllText(LogFile, content);
Console.WriteLine($"[线程{threadId}] 成功写入文件!");
// 模拟处理耗时
Thread.Sleep(new Random().Next(500, 1500));
return; // 成功完成
}
finally
{
mutex.ReleaseMutex();
Console.WriteLine($"[线程{threadId}] 已释放文件锁");
}
}
else
{
Console.WriteLine($"[线程{threadId}] 获取锁超时!");
attempt++;
}
}
}
catch (AbandonedMutexException)
{
Console.WriteLine($"[线程{threadId}] 警告:检测到被遗弃的锁,自动回收!");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine($"[线程{threadId}] 错误:需要管理员权限!");
break;
}
catch (Exception ex)
{
Console.WriteLine($"[线程{threadId}] 未知错误:{ex.Message}");
break;
}
}
Console.WriteLine($"[线程{threadId}] 终止写入操作");
}
}
代码详解:
1. 初始化设置
// 共享文件名
private const string LogFile = "shared_log.txt";
// 全局 Mutex 名称
private const string MutexName = "Global\\MyGlobalFileMutex";
- 使用
Global\\
前缀确保跨进程可见(Windows 系统需管理员权限) - 日志文件 shared_log.txt 会被多个线程/进程追加内容
2. 主逻辑流程
// 用户输入线程数 -> 初始化文件 -> 启动多个写入线程
new Thread(() => WriteToFile(threadId)).Start();
- 每个线程模拟独立进程/线程的写入操作
- 线程间启动间隔 100ms 模拟真实场景的随机性
3. 核心写入方法
using (Mutex mutex = new Mutex(false, MutexName))
{
if (mutex.WaitOne(TimeSpan.FromSeconds(3)))
{
try
{
// 安全写入文件
File.AppendAllText(...);
}
finally
{
mutex.ReleaseMutex();
}
}
}
- using 块确保 Mutex 资源释放
- WaitOne(3000) 设置 3 秒超时避免无限阻塞
- finally 确保锁释放
4. 异常处理
catch (AbandonedMutexException) // 处理前一个持有者异常退出的情况
catch (UnauthorizedAccessException) // 权限不足
catch (Exception ex) // 其他未知错误
- 明确处理常见异常类型
- 提供重试机制(最多尝试 3 次)
运行效果演示
场景1:单进程多线程
=== 跨进程安全文件写入演示 ===
输入要模拟的写入线程数量(建议3-5):
3
[线程1] 尝试获取文件锁... (尝试 1/3)
[线程2] 尝试获取文件锁... (尝试 1/3)
[线程3] 尝试获取文件锁... (尝试 1/3)
[线程1] 成功写入文件!
[线程1] 已释放文件锁
[线程3] 成功写入文件!
[线程3] 已释放文件锁
[线程2] 成功写入文件!
[线程2] 已释放文件锁
生成文件内容 (shared_log.txt):
日志初始化时间: 14:25:36.112
进程 1234 - 线程 1 于 14:25:36.345 写入
进程 1234 - 线程 3 于 14:25:36.901 写入
进程 1234 - 线程 2 于 14:25:37.412 写入
场景2:多进程竞争
同时运行两个实例:
实例1输出:
[线程1] 成功写入文件!
[线程1] 已释放文件锁
[线程2] 获取锁超时!
[线程2] 尝试获取文件锁... (尝试 2/3)
实例2输出:
[线程1] 获取锁超时!
[线程1] 尝试获取文件锁... (尝试 2/3)
[线程1] 成功写入文件!
[线程1] 已释放文件锁
生成文件内容无交错,体现互斥效果。
关键注意事项
- 管理员权限需求:
- 在 Windows 上使用
Global\\
前缀时,需以管理员身份运行程序 - 解决方法:在项目属性中勾选【启用 ClickOnce 安全设置】→【以管理员身份运行】
- 跨平台行为:
// Linux/macOS 的 Mutex 名称会映射到文件系统路径
// private const string MutexName = "/tmp/MyGlobalFileMutex";
- 文件路径权限:
- 确保所有进程对 shared_log.txt 有写入权限
- 建议使用绝对路径(如 C:\logs\shared.log)
- 性能优化
// 高频写入场景可优化为批量写入
var logs = new List<string>();
logs.Add("内容1");
logs.Add("内容2");
File.AppendAllLines(LogFile, logs);
- 扩展实践建议
- 添加日志滚动:
// 每天创建新文件
string LogFile = $"log_{DateTime.Today:yyyyMMdd}.txt";
- 实现优先级写入
if (mutex.WaitOne(0)) // 非阻塞尝试
{
// 紧急日志立即写入
}
- 结合 FileStream 控制缓存:
using (var fs = new FileStream(LogFile, FileMode.Append, FileAccess.Write, FileShare.Read))
using (var writer = new StreamWriter(fs))
{
writer.AutoFlush = true; // 实时写入
writer.WriteLine(content);
}
六、与Monitor的区别
特性 | Mutex | Monitor |
---|---|---|
作用范围 | 跨进程 | 同一进程内 |
锁粒度 | 较粗(系统级资源) | 较细(轻量级对象锁) |
性能开销 | 高(涉及内核切换) | 低(用户模式实现) |
等待机制 | 支持超时、跨进程等待 | 仅支持同一进程内的条件等待 |
异常处理 | 需处理AbandonedMutexException | 无类似异常 |
七、initiallyOwned参数的使用
1. initiallyOwned: true 的适用场景
当 创建线程需要立即持有锁,且希望 直接控制资源访问权 时使用。常见场景如下:
(1) 单实例应用程序
bool isNewInstance;
using (Mutex mutex = new Mutex(
initiallyOwned: true,
name: "Global\\MyAppMutex",
createdNew: out isNewInstance))
{
if (!isNewInstance)
{
Console.WriteLine("程序已在运行!");
Environment.Exit(0);
}
// 主程序逻辑...
}
- 作用:程序启动时立即尝试获取锁,若 isNewInstance 为 false,说明其他实例正在运行。
- 关键点:锁会一直持有到程序退出,无需手动释放(操作系统自动回收)。
(2) 初始化独占资源
// 创建锁并立即获取所有权
Mutex mutex = new Mutex(true, "Global\\SharedResourceMutex");
try
{
if (mutex.WaitOne(0)) // 确认已获得所有权
{
InitializeExclusiveResource(); // 初始化需要独占访问的资源
}
}
finally
{
mutex.ReleaseMutex(); // 初始化完成后释放,允许其他线程访问
}
- 作用:确保资源的初始化过程不被其他线程干扰。
2. initiallyOwned: false 的适用场景
当 创建线程不需要立即持有锁,而是希望后续通过 WaitOne 动态获取时使用。常见场景如下:
(1) 跨进程共享资源的动态控制
Mutex mutex = new Mutex(false, "Global\\SharedFileMutex");
try
{
if (mutex.WaitOne(5000)) // 等待锁的获取
{
File.AppendAllText("data.log", "写入内容...");
}
else
{
Console.WriteLine("获取锁超时!");
}
}
finally
{
mutex.ReleaseMutex(); // 必须显式释放
}
- 作用:允许多个进程/线程按需竞争锁,灵活控制资源访问。
(2) 锁的延迟获取
// 创建锁但不立即持有
Mutex mutex = new Mutex(false, "Global\\DelayedLock");
// ...其他初始化操作...
// 在需要时再获取锁
if (mutex.WaitOne(1000))
{
try
{
// 操作共享资源
}
finally
{
mutex.ReleaseMutex();
}
}
- 作用:推迟锁的获取时机,减少不必要的锁持有时间。
3. 核心原则总结
参数值 | 适用场景 | 注意事项 |
---|---|---|
initiallyOwned: true | 1. 单实例应用启动时立即锁定 2. 需要独占初始化资源 3. 明确要求创建即拥有锁 | 必须检查 createdNew 参数,确保锁实际被获取。 若未获取成功,当前线程不持有锁。 |
initiallyOwned: false | 1. 按需动态获取锁 2. 跨进程协作控制 3. 避免初始化时立即阻塞 | 必须后续调用 WaitOne 获取锁, 并严格通过 try-finally 确保释放。 |
4. 常见误区与验证
误区1:initiallyOwned: true 一定能获取锁
// 错误示例:忽略 createdNew 参数
Mutex mutex = new Mutex(true, "Global\\MyMutex");
// 如果其他实例已持有该锁,此处 createdNew 为 false,但构造函数不会抛出异常!
- 修正:必须检查 createdNew 参数:
bool isNewInstance;
var mutex = new Mutex(true, "Global\\MyMutex", out isNewInstance);
if (!isNewInstance)
{
Console.WriteLine("锁已被其他线程/进程持有!");
}
误区2:不释放 initiallyOwned: true 的锁
// 错误示例:单实例应用中不释放锁(但实际是安全的)
using (Mutex mutex = new Mutex(true, "Global\\MyAppMutex", out isNewInstance))
{
// 程序运行期间一直持有锁
}
// 程序退出时操作系统自动释放,无需手动 ReleaseMutex()
结论:单实例场景中无需手动释放,但动态锁控制场景必须显式释放。
5. 选择参数的决策流程
- 是否需要立即阻止其他实例/线程访问资源?
- 是 → initiallyOwned: true(如单实例应用)。
- 否 → initiallyOwned: false(如动态资源控制)。
- 锁的持有时间是否与程序生命周期一致?
- 是 → initiallyOwned: true,依赖系统自动释放。
- 否 → initiallyOwned: false,手动控制锁的获取/释放。
- 是否需要跨进程协作?
- 是 → 优先使用 initiallyOwned: false,按需竞争锁。
- 否 → 根据场景灵活选择。
6. 验证代码示例
using System;
using System.Threading;
class Program
{
static void Main()
{
// 场景1:单实例应用(initiallyOwned: true)
bool isNewInstance;
using (Mutex mutex = new Mutex(true, "Global\\TestMutex", out isNewInstance))
{
if (!isNewInstance)
{
Console.WriteLine("已有实例运行!");
return;
}
Console.WriteLine("首次启动,持有锁...");
Console.ReadKey();
}
// 场景2:动态锁控制(initiallyOwned: false)
using (Mutex dynamicMutex = new Mutex(false, "Global\\DynamicMutex"))
{
if (dynamicMutex.WaitOne(2000))
{
try
{
Console.WriteLine("动态获取锁成功!");
}
finally
{
dynamicMutex.ReleaseMutex();
}
}
}
}
}
7. 小结
- initiallyOwned: true:适用于 立即锁定 且 持有锁至程序退出 的场景(如单实例应用)。
- initiallyOwned: false:适用于 按需获取锁 的场景(如跨进程资源共享)。
- 关键区别在于 锁的初始化所有权归属,需结合 createdNew 参数和实际业务逻辑判断。
结语
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
Mutexes
Mutex 锁