1. 互斥体概述
互斥体(Mutex,Mutual Exclusion Object)是 Windows 操作系统提供的一种内核同步对象,用于确保多个线程(或进程)对共享资源的互斥访问。它保证在任何时刻,最多只有一个线程能够拥有该互斥体,从而保护临界区代码不被并发执行,避免数据竞争和不一致。
1.1 核心特性
-
互斥性:同一时间只能有一个线程/进程持有互斥体。
-
可跨进程:互斥体是内核对象,可以通过命名在不同进程间共享,实现跨进程同步。
-
递归所有权:同一个线程可以多次获取同一个互斥体(需要释放相同次数),不会导致自身死锁。
-
遗弃检测:如果持有互斥体的线程意外终止而未释放,等待该互斥体的线程会收到
WAIT_ABANDONED通知,从而有机会进行资源清理。 -
超时等待:等待互斥体时可以指定超时时间,避免无限期阻塞。
-
与线程关联:互斥体与获得它的线程相关联,只有拥有它的线程才能释放。
1.2 互斥体 vs 临界区
| 特性 | 互斥体 (Mutex) | 临界区 (Critical Section) |
|---|---|---|
| 类型 | 内核对象 | 用户态对象 |
| 速度 | 较慢(需进入内核) | 非常快(无内核切换) |
| 跨进程 | 支持(命名互斥体) | 不支持 |
| 递归所有权 | 支持 | 支持(需初始化时设置,较新系统默认支持) |
| 超时等待 | 支持(WaitForSingleObject) | 不支持(但可用 TryEnterCriticalSection) |
| 遗弃检测 | 支持(WAIT_ABANDONED) | 不支持 |
| 适用范围 | 跨进程、需要超时/遗弃检测的场景 | 单进程内高性能同步 |
选择依据:单进程内优先使用临界区(性能高);需要跨进程、超时或遗弃检测时使用互斥体。
2. 互斥体的核心 API
2.1 创建互斥体 —— CreateMutex
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性(可继承、权限)
BOOL bInitialOwner, // 是否初始拥有互斥体
LPCTSTR lpName // 互斥体名称(NULL=匿名)
);
-
lpMutexAttributes:指向
SECURITY_ATTRIBUTES结构的指针,可控制句柄继承和设置安全描述符。通常为NULL使用默认安全属性。 -
bInitialOwner:
-
TRUE:调用线程立即拥有该互斥体,互斥体状态为无信号(被占用)。 -
FALSE:互斥体初始为有信号(无人占用)。
-
-
lpName:命名互斥体的名称字符串。名称可以包含
Global\或Local\前缀以控制会话范围。如果指定名称且互斥体已存在,函数返回已存在互斥体的句柄,并可通过GetLastError检查是否创建了新的(ERROR_ALREADY_EXISTS)。 -
返回值:成功返回互斥体句柄,失败返回
NULL。
2.2 打开已存在的互斥体 —— OpenMutex
HANDLE OpenMutex(
DWORD dwDesiredAccess, // 访问权限(如 MUTEX_ALL_ACCESS、SYNCHRONIZE)
BOOL bInheritHandle, // 句柄是否可继承
LPCTSTR lpName // 互斥体名称
);
用于获取一个已创建的命名互斥体句柄,常用于跨进程同步。
-
dwDesiredAccess:通常使用
MUTEX_ALL_ACCESS获取所有权限,或SYNCHRONIZE仅用于等待和释放。需根据实际需求选择。 -
如果互斥体不存在,函数失败,
GetLastError返回ERROR_FILE_NOT_FOUND。
2.3 释放互斥体 —— ReleaseMutex
BOOL ReleaseMutex(HANDLE hMutex);
-
调用线程必须拥有该互斥体,否则函数失败。
-
如果线程多次获取同一互斥体(递归),必须调用相同次数
ReleaseMutex才能真正释放(使互斥体变为有信号)
2.4 等待互斥体 —— WaitForSingleObject 等
线程通过等待函数请求拥有互斥体:
DWORD WaitForSingleObject(
HANDLE hHandle, // 互斥体句柄
DWORD dwMilliseconds // 超时时间(毫秒)
);
-
如果互斥体当前无人拥有(有信号),等待函数立即返回
WAIT_OBJECT_0,调用线程获得所有权(互斥体变为无信号)。 -
如果互斥体已被其他线程拥有(无信号),线程阻塞直到互斥体被释放或超时。
-
如果等待期间互斥体的拥有者线程终止(未释放互斥体),等待函数返回
WAIT_ABANDONED,表示获得了被遗弃的互斥体。
也可使用 WaitForMultipleObjects 等待多个对象,包含互斥体。
2.5 关闭句柄 —— CloseHandle
减少互斥体对象的引用计数,当计数为0时内核对象被销毁。
3. 互斥体的使用步骤
-
创建或打开互斥体:
-
单进程内:使用
CreateMutex创建匿名互斥体(lpName = NULL)。 -
跨进程:一个进程创建命名互斥体,其他进程通过
OpenMutex打开。
-
-
等待互斥体:在访问共享资源前调用
WaitForSingleObject请求所有权。 -
访问共享资源:执行需要保护的代码(临界区)。
-
释放互斥体:访问完毕后调用
ReleaseMutex。 -
关闭句柄:程序结束或不再需要互斥体时,调用
CloseHandle。
4. 详细示例
4.1 单进程内多线程互斥
// SingleProcessMutex.cpp
#include <windows.h>
#include <stdio.h>
HANDLE g_hMutex;
int g_counter = 0;
const int ITERATIONS = 1000000;
DWORD WINAPI ThreadProc(LPVOID lpParam) {
for (int i = 0; i < ITERATIONS; i++) {
WaitForSingleObject(g_hMutex, INFINITE); // 请求互斥体
g_counter++;
ReleaseMutex(g_hMutex);
}
return 0;
}
int main() {
// 创建匿名互斥体,初始无人拥有
g_hMutex = CreateMutex(NULL, FALSE, NULL);
if (!g_hMutex) {
printf("CreateMutex failed: %d\n", GetLastError());
return 1;
}
HANDLE hThreads[4];
for (int i = 0; i < 4; i++) {
hThreads[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
}
WaitForMultipleObjects(4, hThreads, TRUE, INFINITE);
printf("Expected counter: %d\n", 4 * ITERATIONS);
printf("Actual counter: %d\n", g_counter);
for (int i = 0; i < 4; i++) CloseHandle(hThreads[i]);
CloseHandle(g_hMutex);
return 0;
}
4.2 跨进程互斥(命名互斥体)
进程A(创建并拥有互斥体)
// ProcessA.cpp
#include <windows.h>
#include <stdio.h>
int main() {
// 创建命名互斥体,初始由本进程拥有
HANDLE hMutex = CreateMutex(NULL, TRUE, TEXT("Global\\MyCrossProcessMutex"));
if (!hMutex) {
printf("CreateMutex failed: %d\n", GetLastError());
return 1;
}
printf("进程 A 拥有互斥体。按任意键释放...\n");
getchar();
ReleaseMutex(hMutex);
printf("互斥体已释放。\n");
CloseHandle(hMutex);
return 0;
}
进程B(等待互斥体)
// ProcessB.cpp
#include <windows.h>
#include <stdio.h>
int main() {
// 打开命名互斥体
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, TEXT("Global\\MyCrossProcessMutex"));
if (!hMutex) {
printf("OpenMutex failed: %d\n", GetLastError());
return 1;
}
printf("进程 B 等待互斥体...\n");
DWORD dwWait = WaitForSingleObject(hMutex, INFINITE);
if (dwWait == WAIT_OBJECT_0) {
printf("进程 B 获得互斥体,执行临界操作...\n");
// 模拟工作
Sleep(2000);
ReleaseMutex(hMutex);
printf("进程 B 释放互斥体。\n");
} else if (dwWait == WAIT_ABANDONED) {
printf("警告:获得被遗弃的互斥体,数据可能不一致。\n");
// 可以尝试清理,然后释放
ReleaseMutex(hMutex);
} else {
printf("等待失败: %d\n", GetLastError());
}
CloseHandle(hMutex);
return 0;
}
说明:
-
命名互斥体使用
Global\前缀,可在不同会话间共享(如服务与用户进程)。若不需要跨会话,可使用Local\或省略(默认Local\)。 -
进程A初始拥有互斥体,进程B等待。当进程A释放后,进程B获得。
4.3 递归所有权
// MutexRecursive.cpp
#include <windows.h>
#include <stdio.h>
HANDLE g_hMutex;
void RecursiveFunction(int depth) {
if (depth <= 0) return;
WaitForSingleObject(g_hMutex, INFINITE);
printf("Depth %d: got mutex\n", depth);
RecursiveFunction(depth - 1); // 递归调用,再次请求互斥体
printf("Depth %d: releasing mutex\n", depth);
ReleaseMutex(g_hMutex);
}
int main() {
g_hMutex = CreateMutex(NULL, FALSE, NULL); // 初始无人拥有
if (!g_hMutex) return 1;
HANDLE hThread = CreateThread(NULL, 0, [](LPVOID) -> DWORD {
RecursiveFunction(3);
return 0;
}, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(g_hMutex);
return 0;
}
输出:
Depth 3: got mutex
Depth 2: got mutex
Depth 1: got mutex
Depth 1: releasing mutex
Depth 2: releasing mutex
Depth 3: releasing mutex
说明:同一线程可以多次获取同一个互斥体,释放次数必须匹配。
4.4 处理互斥体遗弃
当拥有互斥体的线程意外终止(如崩溃、被终止),互斥体不会被自动释放。系统会将其标记为“遗弃”,并释放所有权给下一个等待线程,同时返回 WAIT_ABANDONED。示例见进程B代码中的 WAIT_ABANDONED 处理。收到该返回值后,应用程序应意识到共享资源可能处于不一致状态,需谨慎恢复或清理。
4.5 避免死锁(多互斥体顺序)
当需要保护多个资源时,如果两个线程以不同顺序请求互斥体,可能发生死锁。解决方法:所有线程按相同顺序请求互斥体。
// DeadlockFree.cpp
#include <windows.h>
#include <stdio.h>
HANDLE g_hMutexA, g_hMutexB;
DWORD WINAPI Thread1(LPVOID) {
// 总是先A后B
WaitForSingleObject(g_hMutexA, INFINITE);
WaitForSingleObject(g_hMutexB, INFINITE);
printf("Thread1: got both mutexes\n");
ReleaseMutex(g_hMutexB);
ReleaseMutex(g_hMutexA);
return 0;
}
DWORD WINAPI Thread2(LPVOID) {
// 同样先A后B
WaitForSingleObject(g_hMutexA, INFINITE);
WaitForSingleObject(g_hMutexB, INFINITE);
printf("Thread2: got both mutexes\n");
ReleaseMutex(g_hMutexB);
ReleaseMutex(g_hMutexA);
return 0;
}
int main() {
g_hMutexA = CreateMutex(NULL, FALSE, NULL);
g_hMutexB = CreateMutex(NULL, FALSE, NULL);
HANDLE hThreads[2];
hThreads[0] = CreateThread(NULL, 0, Thread1, NULL, 0, NULL);
hThreads[1] = CreateThread(NULL, 0, Thread2, NULL, 0, NULL);
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
CloseHandle(g_hMutexA);
CloseHandle(g_hMutexB);
return 0;
}
5. 命名互斥体的命名空间与安全性
5.1 命名空间前缀
-
Local\:默认前缀(如果省略,系统也会视为Local\)。互斥体只在当前会话中可见。对于服务(Session 0)与用户应用(Session 1)通信,需使用Global\。 -
Global\:在所有会话中可见,可用于全局同步。需要适当权限,低权限进程可能无法访问。 -
Session\:特定会话,较少使用。
5.2 安全描述符
通过 CreateMutex 的 lpMutexAttributes 参数可以指定 SECURITY_ATTRIBUTES,其中包含一个安全描述符,限制哪些用户或组可以访问、修改互斥体。例如,创建一个只有管理员可以打开的互斥体。
6. 互斥体的内部机制(简述)
互斥体作为内核对象,其内部维护:
-
引用计数:由
CloseHandle管理。 -
拥有者线程 ID:记录当前拥有它的线程。
-
递归计数:同一线程获取次数。
-
等待队列:等待该互斥体的线程列表。
当线程调用 WaitForSingleObject 时,如果互斥体未被占用,系统立即授予所有权,否则将线程放入等待队列。当拥有者调用 ReleaseMutex 且递归计数减到0时,系统从等待队列中唤醒一个线程(通常是等待时间最长的),该线程获得所有权。
7. 注意事项与最佳实践
-
始终配对等待和释放:确保每条执行路径(包括异常分支)都调用
ReleaseMutex。在 C++ 中,可以使用 RAII 封装(如std::lock_guard或自定义类)自动管理。 -
避免长时间持有互斥体:临界区代码应尽量简短,减少其他线程的等待时间,避免性能瓶颈。
-
处理遗弃情况:如果等待返回
WAIT_ABANDONED,说明互斥体被遗弃,应进行错误恢复,如重置资源、记录日志或终止程序。 -
死锁预防:
-
按固定顺序获取多个互斥体。
-
使用超时等待,超时后释放已获得的互斥体并重试。
-
考虑使用
WaitForMultipleObjects同时等待所有需要的互斥体(但要注意自动重置对象的副作用)。
-
-
命名互斥体冲突:命名互斥体使用全局名称,应确保名称唯一性(如包含公司名、应用名、GUID),避免与其他应用冲突。
-
访问权限:跨进程互斥体时,确保所有进程都有适当的访问权限(至少
SYNCHRONIZE用于等待,MUTEX_MODIFY_STATE用于释放)。通常用MUTEX_ALL_ACCESS简化。 -
递归限制:虽然互斥体支持递归,但递归会增加复杂性,尽量设计非递归的锁使用模式。
-
性能考量:单进程内高频率同步建议使用临界区;互斥体适用于跨进程或需要遗弃检测的场景。
8. 互斥体与其他同步对象的对比
| 对象 | 用途 | 特点 |
|---|---|---|
| 互斥体 (Mutex) | 互斥访问(可跨进程) | 内核对象,递归,遗弃检测,可命名 |
| 临界区 (Critical Section) | 单进程内互斥 | 用户态,速度快,不能跨进程 |
| 信号量 (Semaphore) | 资源计数 | 允许多个线程同时访问,有最大计数 |
| 事件 (Event) | 通知条件发生 | 手动/自动重置,适合信号传递 |
| 读写锁 (SRWLock) | 读多写少场景 | 用户态,读写分离,性能高 |
&spm=1001.2101.3001.5002&articleId=158471910&d=1&t=3&u=2d0ae96dce5546ad8636f77c739e8963)
8567

被折叠的 条评论
为什么被折叠?



