Windows学习笔记-21(线程同步-互斥体Mutex)

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. 互斥体的使用步骤

  1. 创建或打开互斥体

    • 单进程内:使用 CreateMutex 创建匿名互斥体(lpName = NULL)。

    • 跨进程:一个进程创建命名互斥体,其他进程通过 OpenMutex 打开。

  2. 等待互斥体:在访问共享资源前调用 WaitForSingleObject 请求所有权。

  3. 访问共享资源:执行需要保护的代码(临界区)。

  4. 释放互斥体:访问完毕后调用 ReleaseMutex

  5. 关闭句柄:程序结束或不再需要互斥体时,调用 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. 注意事项与最佳实践

  1. 始终配对等待和释放:确保每条执行路径(包括异常分支)都调用 ReleaseMutex。在 C++ 中,可以使用 RAII 封装(如 std::lock_guard 或自定义类)自动管理。

  2. 避免长时间持有互斥体:临界区代码应尽量简短,减少其他线程的等待时间,避免性能瓶颈。

  3. 处理遗弃情况:如果等待返回 WAIT_ABANDONED,说明互斥体被遗弃,应进行错误恢复,如重置资源、记录日志或终止程序。

  4. 死锁预防

    • 按固定顺序获取多个互斥体。

    • 使用超时等待,超时后释放已获得的互斥体并重试。

    • 考虑使用 WaitForMultipleObjects 同时等待所有需要的互斥体(但要注意自动重置对象的副作用)。

  5. 命名互斥体冲突:命名互斥体使用全局名称,应确保名称唯一性(如包含公司名、应用名、GUID),避免与其他应用冲突。

  6. 访问权限:跨进程互斥体时,确保所有进程都有适当的访问权限(至少 SYNCHRONIZE 用于等待,MUTEX_MODIFY_STATE 用于释放)。通常用 MUTEX_ALL_ACCESS 简化。

  7. 递归限制:虽然互斥体支持递归,但递归会增加复杂性,尽量设计非递归的锁使用模式。

  8. 性能考量:单进程内高频率同步建议使用临界区;互斥体适用于跨进程或需要遗弃检测的场景。

8. 互斥体与其他同步对象的对比

对象用途特点
互斥体 (Mutex)互斥访问(可跨进程)内核对象,递归,遗弃检测,可命名
临界区 (Critical Section)单进程内互斥用户态,速度快,不能跨进程
信号量 (Semaphore)资源计数允许多个线程同时访问,有最大计数
事件 (Event)通知条件发生手动/自动重置,适合信号传递
读写锁 (SRWLock)读多写少场景用户态,读写分离,性能高

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值