一、Windows线程编程:从入门到实战

大家好,我是你们的老朋友。今天咱们聊聊Windows下的线程编程。说实话,我刚开始接触多线程时,也被一堆API搞得晕头转向。但别怕,跟着我一步步来,你会发现其实没那么复杂。

1.1 Windows线程API入门

Windows提供了两套线程创建API:CreateThread_beginthreadex。很多人会问:到底该用哪个?

核心结论:在C/C++程序中,永远用_beginthreadex,别用CreateThread。

为什么?我当年刚入行时也踩过这个坑。用CreateThread创建线程,程序跑着跑着就莫名其妙崩溃了。后来才发现,CreateThread不会初始化C运行时库的线程局部存储(TLS),导致errnostrtok这类函数出问题。

来看个对比:

// ❌ 不推荐:CreateThread
HANDLE hThread = CreateThread(
    NULL,           // 安全属性
    0,              // 栈大小(默认)
    ThreadFunc,     // 线程函数
    param,          // 参数
    0,              // 创建标志
    &threadId       // 线程ID
);

// ✅ 推荐:_beginthreadex
unsigned long hThread = _beginthreadex(
    NULL,           // 安全属性
    0,              // 栈大小
    ThreadFunc,     // 线程函数(注意返回类型是unsigned)
    param,          // 参数
    0,              // 创建标志
    &threadId       // 线程ID
);

小提示:_beginthreadex返回的是uintptr_t,不是HANDLE。但你可以直接把它当作HANDLE用,比如传给WaitForSingleObject。

1.2 CreateThread与_beginthreadex的恩怨情仇

说白了,_beginthreadex就是对CreateThread的封装。它额外做了三件事:

  • 分配线程局部存储(TLS)
  • 初始化C运行时库的状态
  • 处理异常安全

我曾经在一个项目中,同事图省事用了CreateThread,结果在调用printf时偶尔崩溃。查了两天才定位到问题——就是线程局部存储没初始化。从那以后,我团队里就立了规矩:C/C++线程创建,一律用_beginthreadex

对了,还有个细节:_beginthreadex的线程函数返回值是unsigned (__stdcall *)(void*),别写错了。

1.3 Windows同步对象:三剑客

多线程编程,说白了就是「抢资源」和「等信号」。Windows提供了三种最常用的同步对象,我按使用频率排个序:

同步对象 适用场景 性能 我常用的场景
CriticalSection 同一进程内线程互斥 最快 保护全局变量、缓存
Event 线程间信号通知 中等 生产者-消费者模式
Mutex 跨进程同步 较慢 多进程共享资源

CriticalSection(临界区)

这是我最常用的同步对象。它轻量、快速,只在用户态工作。来看个典型用法:

// 初始化
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);

// 使用
EnterCriticalSection(&cs);
// 访问共享资源...
LeaveCriticalSection(&cs);

// 销毁
DeleteCriticalSection(&cs);

注意:CriticalSection不能跨进程使用。另外,EnterCriticalSection和LeaveCriticalSection必须成对出现,否则会造成死锁。我见过有人忘记Leave,结果线程卡死——嗯,调试这种问题最头疼了。

Event(事件)

Event适合做「信号通知」。比如一个线程等数据,另一个线程生产数据。我常用的是自动重置事件:

// 创建自动重置事件
HANDLE hEvent = CreateEvent(
    NULL,       // 安全属性
    FALSE,      // 自动重置(TRUE=手动重置)
    FALSE,      // 初始无信号
    NULL        // 事件名
);

// 生产者线程
SetEvent(hEvent);  // 发信号

// 消费者线程
WaitForSingleObject(hEvent, INFINITE);  // 等信号

手动重置事件和自动重置事件的区别?简单说:自动重置就像「单次门禁」,一个人通过后门就关了;手动重置像「大门」,开了以后所有人都能进,直到你手动关。

Mutex(互斥量)

Mutex和CriticalSection功能类似,但Mutex可以跨进程使用。不过性能差一些,因为涉及内核态切换。我一般只在需要跨进程同步时才用Mutex。

// 创建命名Mutex(可跨进程)
HANDLE hMutex = CreateMutex(
    NULL,           // 安全属性
    FALSE,          // 初始不占用
    "Global\\MyMutex"  // 名称
);

// 等待
WaitForSingleObject(hMutex, INFINITE);
// 访问共享资源...
ReleaseMutex(hMutex);

1.4 Windows线程池

手动创建线程管理起来太麻烦?Windows提供了线程池(Thread Pool),从Vista开始就内置了。我个人非常推荐用线程池,尤其是处理大量短任务时。

线程池的核心API:

// 提交一个工作项到线程池
BOOL QueueUserWorkItem(
    LPTHREAD_START_ROUTINE Function,  // 回调函数
    PVOID Context,                    // 参数
    ULONG Flags                       // 标志
);

// 示例:简单使用
DWORD WINAPI MyWork(PVOID param) {
    // 干活...
    return 0;
}

QueueUserWorkItem(MyWork, NULL, WT_EXECUTEDEFAULT);

经验之谈:线程池会自动管理线程数量,根据CPU核心数和任务负载动态调整。你只管提交任务,不用操心线程创建和销毁。我做过一个图片处理服务,用线程池后性能提升了30%,代码还少了一半。

线程池还有更高级的用法,比如定时器、IO完成端口等。不过咱们先掌握基础,后面章节再深入。

1.5 知识体系总览

下面这张图总结了本章的核心内容,我建议你保存下来,以后写代码时对照着看:

Windows线程编程知识体系 线程创建 CreateThread vs _beginthreadex 同步对象三剑客 CriticalSection Event Mutex Windows线程池 (QueueUserWorkItem) 核心原则:_beginthreadex + CriticalSection + 线程池

1.6 避坑指南

最后,分享几个我这些年踩过的坑:

  • 忘记关闭线程句柄:CreateThread/_beginthreadex返回的句柄需要CloseHandle,否则会泄漏内核对象。我见过一个服务器程序跑了三天后句柄耗尽崩溃——嗯,就是这个问题。
  • 临界区未初始化就使用:InitializeCriticalSection一定要在EnterCriticalSection之前调用。别问我怎么知道的。
  • Event信号丢失:如果使用自动重置事件,确保在WaitForSingleObject返回后立即处理,否则可能被其他线程抢走信号。
  • 线程池回调中做耗时操作:线程池的线程是共享的,别在里面做阻塞操作(比如Sleep、WaitForSingleObject),否则会阻塞其他任务。

好了,这一章就到这里。Windows线程编程入门其实不难,关键是理解每种工具的适用场景。下一章咱们聊聊线程同步的进阶话题——死锁检测和锁的粒度控制。


公众号:蓝海资料掘金营,微信deep3321