使用内核对象进行线程同步

使用内核对象进行线程同步

在多线程编程中,同步机制帮助线程协调彼此的工作。Windows提供了两套不同的方案:用户模式同步和内核对象同步,每种方案都有其适用的场景。

内核对象同步与用户模式同步

理解这两种同步方式的差异很重要。用户模式同步,比如关键段,在进程内部工作,不涉及系统内核。它们通过原子操作实现同步,通常速度很快。但这种方法只能在同一进程内使用,缺少超时机制,而且如果持有锁的线程意外终止,其他线程可能无法继续执行。

内核对象同步采用不同的方式。事件、互斥量和信号量等对象由操作系统内核管理,可以在不同进程间共享,可以设置等待时间,也可以命名以便识别。更重要的是,系统能管理这些对象的生命周期,当持有互斥量的线程意外结束时,系统会自动释放这个互斥量,避免其他线程永久等待。

这种功能上的增强是有代价的。每次线程等待内核对象时,都需要从用户模式切换到内核模式,这种切换需要时间。因此,在需要频繁同步的场景中,用户模式同步通常更有效率;而需要跨进程协作或更复杂控制时,内核对象同步是必要的选择。

事件内核对象

事件是Windows中最基础的同步对象之一,它的行为很简单:线程可以等待某个事件发生,其他线程可以触发这个事件。根据事件被触发后的行为,Windows提供两种类型的事件。

手动重置事件在触发后会一直保持触发状态,直到被明确重置。这意味着当这个事件被触发时,所有正在等待它的线程都会被唤醒,而且之后开始等待的线程也会立即继续执行。

c 复制代码
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (hEvent == NULL) {
    // 处理创建失败
}

// 在一个线程中等待事件
DWORD WaitResult = WaitForSingleObject(hEvent, INFINITE);
if (WaitResult == WAIT_OBJECT_0) {
    // 事件被触发
}

// 在另一个线程中触发事件
SetEvent(hEvent);

自动重置事件的行为不同。当它被触发时,只唤醒一个正在等待的线程,然后自动恢复到未触发状态。这适合那种一次只能有一个线程继续执行的场景。

c 复制代码
HANDLE hAutoEvent = CreateEvent(NULL, FALSE, FALSE, NULL);

// 多个线程可以这样等待
DWORD result = WaitForSingleObject(hAutoEvent, 5000); // 等待5秒
if (result == WAIT_TIMEOUT) {
    // 超时处理
}

计时器内核对象

计时器对象让线程可以在特定时间点被唤醒,或者按固定间隔重复执行。这在需要定期执行任务或延迟执行的场景中很有用。

c 复制代码
// 创建一个在5秒后触发的计时器
HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, TEXT("MyTimer"));
if (hTimer != NULL) {
    LARGE_INTEGER liDueTime;
    liDueTime.QuadPart = -50000000; // 5秒后(100纳秒单位)
    
    SetWaitableTimer(hTimer, &liDueTime, 0, NULL, NULL, FALSE);
    
    // 等待计时器触发
    WaitForSingleObject(hTimer, INFINITE);
    
    CloseHandle(hTimer);
}

计时器有同步和异步两种使用方式。同步使用时,线程等待计时器触发;异步使用时,可以结合I/O完成端口或可等待计时器与工作线程配合。

信号量内核对象

信号量用于控制对有限资源的访问。它维护一个计数器,表示当前可用的资源数量。当线程需要资源时,它尝试减少计数器;如果计数器为0,线程需要等待。

c 复制代码
// 创建一个最多允许3个线程同时访问的信号量
HANDLE hSemaphore = CreateSemaphore(NULL, 3, 3, NULL);

// 线程访问资源的代码
DWORD WaitResult = WaitForSingleObject(hSemaphore, 1000); // 等待1秒
if (WaitResult == WAIT_OBJECT_0) {
    // 获得访问权限
    // 使用资源...
    
    // 释放访问权限
    ReleaseSemaphore(hSemaphore, 1, NULL);
} else if (WaitResult == WAIT_TIMEOUT) {
    // 超时,未能获得访问权限
}

信号量的初始计数和最大计数可以分别设置。如果创建一个初始计数为0的信号量,它可以用来表示某个任务尚未完成,多个线程可以等待这个任务,完成后通过增加信号量计数来通知等待的线程。

互斥量内核对象

互斥量确保一次只有一个线程访问受保护的资源。与关键段类似,但互斥量是内核对象,可以跨进程使用,并且有超时机制。

c 复制代码
// 创建一个命名的互斥量,可以跨进程使用
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("Global\\MySharedMutex"));
if (hMutex == NULL) {
    // 处理错误
}

// 尝试获取互斥量
DWORD result = WaitForSingleObject(hMutex, 100);
if (result == WAIT_OBJECT_0) {
    // 成功获取互斥量
    // 访问共享资源...
    
    // 释放互斥量
    ReleaseMutex(hMutex);
} else if (result == WAIT_TIMEOUT) {
    // 超时,未能获取互斥量
} else if (result == WAIT_ABANDONED) {
    // 互斥量被前一个所有者遗弃
    // 需要检查数据一致性
}

CloseHandle(hMutex);

互斥量有一个有用的特性:如果持有互斥量的线程意外终止,系统会将互斥量标记为"遗弃",下一个等待的线程会以特殊状态获得它,这样程序可以检测到异常情况。

二值信号量与互斥量的差异

虽然二值信号量和互斥量在功能上有些相似,但它们的设计目的不同。互斥量用于保护共享资源,它有所有权的概念------通常哪个线程获取了互斥量,就应该由哪个线程释放。信号量则不同,它更像是通行证,任何线程都可以释放信号量。

这种差异在实际使用中很重要。用二值信号量模拟互斥量时,如果获取信号量的线程崩溃了,其他线程可能永远等待。而互斥量在所有者线程崩溃时会被系统自动释放。另外,互斥量通常支持递归获取,同一个线程可以多次获取同一个互斥量而不会死锁,这在实现递归函数时很有用。

其他同步机制

除了上面提到的对象,Windows还提供了其他同步工具。WaitForMultipleObjects函数允许线程同时等待多个对象,这在需要响应多种事件的场景中很有用。

c 复制代码
HANDLE handles[2];
handles[0] = hEvent;  // 一个事件
handles[1] = hMutex;  // 一个互斥量

// 等待任意一个对象
DWORD result = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
switch (result) {
    case WAIT_OBJECT_0:     // 事件触发
        // 处理事件
        break;
    case WAIT_OBJECT_0 + 1: // 互斥量可用
        // 获取互斥量并处理
        ReleaseMutex(handles[1]);
        break;
}

MsgWaitForMultipleObjects在处理Windows消息循环的线程中特别有用。它允许线程在等待内核对象的同时仍然处理消息,这在GUI程序中很常见。

c 复制代码
// 在消息循环中等待对象
while (TRUE) {
    DWORD result = MsgWaitForMultipleObjects(
        1, &hEvent,       // 等待的对象
        FALSE,           // 等待任意一个
        INFINITE,        // 无限等待
        QS_ALLINPUT      // 也处理所有消息
    );
    
    if (result == WAIT_OBJECT_0) {
        // 事件触发
        break;
    } else if (result == WAIT_OBJECT_0 + 1) {
        // 有消息到达
        MSG msg;
        while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
}

这些函数提供了更大的灵活性,但使用起来也更复杂,需要仔细考虑各种返回值和处理逻辑。

选择合适的同步机制需要考虑具体场景。如果需要快速、进程内的同步,用户模式对象通常更好。如果需要跨进程、超时、或更精细的控制,内核对象是合适的选择。理解每种工具的特性和适用场景,才能在实际开发中做出恰当的选择。

相关推荐
张赐荣2 小时前
深入详解在 Python 中用 ctypes 调用 Windows API 清空回收站
开发语言·windows·python
2501_939998202 小时前
Antimalware Service Executable 占用率极高怎么关闭
windows
万粉变现经纪人2 小时前
如何解决 pip install bitsandbytes 报错 仅支持 Linux+glibc(macOS/Windows 失败)问题
linux·运维·windows·python·scrapy·macos·pip
嵌入式Q2 小时前
FreeRTOS源码解析(2)任务挂起与恢复
windows
Hello.Reader3 小时前
Windows C 盘空间告急?用 PowerShell 写一个安全可控的清理脚本
c语言·windows·安全
阿洛学长3 小时前
OpenClaw零成本部署指南:Windows/Mac/Linux/阿里云搭建+两个免费大模型API配置攻略
linux·windows·macos
嵌入式Q3 小时前
FreeRTOS源码解析(2)任务调度器挂起与恢复
windows
飘飘叶4 小时前
[FRP]Windows 安装 frpc 客户端,以及P2P方式ssh配置
windows·frp
承渊政道4 小时前
用群晖部署OmniBox+pansou:把分散的影视资源全聚合到一个界面里
服务器·windows·网络协议·https·ip·视频·持续部署