一、Windows线程编程:从入门到实战
大家好,我是你们的老朋友。今天咱们聊聊Windows下的线程编程。说实话,我刚开始接触多线程时,也被一堆API搞得晕头转向。但别怕,跟着我一步步来,你会发现其实没那么复杂。
1.1 Windows线程API入门
Windows提供了两套线程创建API:CreateThread和_beginthreadex。很多人会问:到底该用哪个?
核心结论:在C/C++程序中,永远用_beginthreadex,别用CreateThread。
为什么?我当年刚入行时也踩过这个坑。用CreateThread创建线程,程序跑着跑着就莫名其妙崩溃了。后来才发现,CreateThread不会初始化C运行时库的线程局部存储(TLS),导致errno、strtok这类函数出问题。
来看个对比:
// ❌ 不推荐: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 知识体系总览
下面这张图总结了本章的核心内容,我建议你保存下来,以后写代码时对照着看:
1.6 避坑指南
最后,分享几个我这些年踩过的坑:
- 忘记关闭线程句柄:CreateThread/_beginthreadex返回的句柄需要CloseHandle,否则会泄漏内核对象。我见过一个服务器程序跑了三天后句柄耗尽崩溃——嗯,就是这个问题。
- 临界区未初始化就使用:InitializeCriticalSection一定要在EnterCriticalSection之前调用。别问我怎么知道的。
- Event信号丢失:如果使用自动重置事件,确保在WaitForSingleObject返回后立即处理,否则可能被其他线程抢走信号。
- 线程池回调中做耗时操作:线程池的线程是共享的,别在里面做阻塞操作(比如Sleep、WaitForSingleObject),否则会阻塞其他任务。
好了,这一章就到这里。Windows线程编程入门其实不难,关键是理解每种工具的适用场景。下一章咱们聊聊线程同步的进阶话题——死锁检测和锁的粒度控制。
公众号:蓝海资料掘金营,微信deep3321