CreateEvent 实现线程同步(Win32)

一、函数原型

HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性,NULL默认
  BOOL                  bManualReset,       // TRUE=手动重置;FALSE=自动重置
  BOOL                  bInitialState,      // 初始状态:TRUE有信号,FALSE无信号
  LPCTSTR               lpName              // 命名事件,NULL匿名(线程间一般匿名)
);

事件两种核心类型:

  1. 手动重置事件(bManualReset=TRUE)SetEvent置为有信号后,一直保持有信号,必须手动调用ResetEvent才变回无信号。多个等待线程可以全部被唤醒。
  2. 自动重置事件(bManualReset=FALSE)SetEvent置有信号,只要唤醒1个等待线程,内核自动把事件变回无信号。适合一对一通知,防止多线程全部冲进去。

等待函数:WaitForSingleObject(hEvent, INFINITE)

  • INFINITE:无限等待,直到事件变为有信号状态才返回。

二、核心原理

事件内核对象有两种状态:有信号 / 无信号

  • 线程调用WaitForSingleObject阻塞,事件无信号时线程挂起;
  • 别的线程调用SetEvent(hEvent) → 事件变成有信号 → 等待线程被唤醒继续执行。

三、示例代码:自动重置事件(一对一通知)

场景:主线程创建子线程,子线程等待事件信号,主线程满足条件后发信号唤醒子线程

#include <windows.h>
#include <stdio.h>

// 事件句柄,全局方便多线程访问
HANDLE g_hEvent;

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    printf("子线程:开始等待事件信号...\n");
    // 阻塞等待事件变为有信号
    WaitForSingleObject(g_hEvent, INFINITE);
    // 自动重置事件:唤醒后自动重置回无信号,不需要ResetEvent
    printf("子线程:收到事件信号,开始执行任务\n");
    return 0;
}

int main()
{
    // 创建【自动重置事件】,初始状态:无信号
    g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    if (g_hEvent == NULL)
    {
        printf("CreateEvent失败,err=%d\n", GetLastError());
        return -1;
    }

    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
    CloseHandle(hThread);

    Sleep(2000); // 模拟主线程做准备工作

    printf("主线程:发送事件信号 SetEvent\n");
    SetEvent(g_hEvent); // 唤醒等待的子线程

    Sleep(1000);
    CloseHandle(g_hEvent); // 释放内核对象
    return 0;
}

四、手动重置事件示例(一次性唤醒多个线程)

#include <windows.h>
#include <stdio.h>

HANDLE g_hEvent;

DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    int id = *(int*)lpParam;
    printf("线程%d 等待事件...\n", id);
    WaitForSingleObject(g_hEvent, INFINITE);
    printf("线程%d 被唤醒!\n", id);
    return 0;
}

int main()
{
    // 手动重置事件,初始无信号
    g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    HANDLE hThreads[3];
    int ids[3] = {1,2,3};
    for(int i=0;i<3;i++){
        hThreads[i] = CreateThread(NULL,0,ThreadProc,&ids[i],0,NULL);
    }
    Sleep(2000);
    printf("主线程:SetEvent,唤醒全部等待线程\n");
    SetEvent(g_hEvent);
    Sleep(1000);
    ResetEvent(g_hEvent); // ⚠️手动重置必须手动关闭信号
    for(int i=0;i<3;i++) CloseHandle(hThreads[i]);
    CloseHandle(g_hEvent);
    return 0;
}

五、常用API清单

  1. SetEvent(hEvent):把事件设置为有信号
  2. ResetEvent(hEvent):把事件设置为无信号(手动事件才需要)
  3. WaitForSingleObject(h, dwMilliseconds):等待单个内核对象
  4. WaitForMultipleObjects():同时等待多个事件/内核对象
  5. CloseHandle(hEvent):必须释放句柄,否则内核资源泄漏

六、两种事件选型要点

类型 bManualReset 特点 使用场景
自动重置 FALSE SetEvent唤醒一个线程,自动变回无信号 生产者通知单个消费者,防止并发重入
手动重置 TRUE SetEvent唤醒所有等待线程,保持有信号 批量唤醒一组线程,需要手动ResetEvent关闭

七、常见坑

  1. 自动重置事件:如果SetEvent的时候没有线程正在等待,信号会丢失。>

    例:主线程先SetEvent,子线程才开始Wait,自动事件的信号直接消失,子线程永久阻塞。手动重置事件不会丢信号,状态会保留。

  2. 忘记CloseHandle:内核对象不会释放,长期运行程序句柄泄漏。
  3. 多线程共享句柄:不要随意跨进程裸传句柄;跨进程要用命名事件或者句柄复制。
  4. 事件不能保护临界区(不能替代临界段/互斥量):事件是通知机制,不是互斥锁。如果要保护一段代码同一时间只能一个线程跑,要用CRITICAL_SECTIONCreateMutex