28、跨平台并发编程:Windows线程API、POSIX线程(pthreads)、跨平台封装策略、使用Boost.Thread
说实话,跨平台并发编程这个话题,我当年刚入行时踩过不少坑。那时候公司产品要同时跑在Windows和Linux上,我天真地以为写两套线程代码就完事了。结果呢?维护成本直接翻倍,还经常出现「Windows上跑得好好的,Linux上一跑就崩」的诡异问题。
后来我学乖了。跨平台并发编程,说白了就三条路:要么直接怼系统API(Windows线程API或pthreads),要么自己封装一层,要么用现成的库(比如Boost.Thread)。今天咱们就把这三条路都走一遍,看看各自的门道。
Windows线程API:CreateThread与它的朋友们
Windows的线程API,核心就是CreateThread。我刚开始用的时候,觉得这函数参数真多,后来用顺手了,反而觉得它设计得挺严谨。
// Windows线程示例
#include <windows.h>
#include <iostream>
DWORD WINAPI ThreadProc(LPVOID lpParam) {
int* data = static_cast<int*>(lpParam);
std::cout << "线程收到数据: " << *data << std::endl;
return 0;
}
int main() {
int value = 42;
HANDLE hThread = CreateThread(
NULL, // 安全属性,默认NULL
0, // 栈大小,0表示默认
ThreadProc, // 线程函数
&value, // 传给线程的参数
0, // 创建标志,0表示立即运行
NULL // 线程ID,不需要可以传NULL
);
if (hThread == NULL) {
std::cerr << "创建线程失败,错误码: " << GetLastError() << std::endl;
return 1;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return 0;
}
我曾经在项目里直接用
_beginthreadex替代CreateThread,因为前者能正确初始化C运行时库。如果你在Windows上写C++多线程,建议用_beginthreadex而不是CreateThread,否则可能会遇到内存泄漏或CRT函数调用异常。
Windows还提供了WaitForMultipleObjects、SetEvent、CreateMutex等同步原语。说实话,这套API虽然功能强大,但命名风格跟C++标准库差得有点远,跨平台移植时改起来挺痛苦的。
POSIX线程(pthreads):Linux/Unix世界的标准
pthreads是POSIX标准定义的线程接口,Linux、macOS、FreeBSD都支持。它的设计哲学跟Windows API完全不同——更简洁,更「函数式」。
// POSIX线程示例
#include <pthread.h>
#include <iostream>
void* ThreadProc(void* arg) {
int* data = static_cast<int*>(arg);
std::cout << "线程收到数据: " << *data << std::endl;
return nullptr;
}
int main() {
pthread_t thread;
int value = 42;
int ret = pthread_create(&thread, nullptr, ThreadProc, &value);
if (ret != 0) {
std::cerr << "创建线程失败,错误码: " << ret << std::endl;
return 1;
}
pthread_join(thread, nullptr);
return 0;
}
你看,pthreads的接口更统一:pthread_create、pthread_join、pthread_mutex_lock……所有函数都带pthread_前缀,一眼就能认出是线程相关。我个人觉得这种命名规范比Windows的CreateThread、WaitForSingleObject更容易记忆。
在Linux上编译pthreads程序时,记得加
-lpthread链接选项。我见过不少新手忘了这个,结果链接报错一脸懵。
跨平台封装策略:自己动手,丰衣足食
如果你不想依赖第三方库,可以自己封装一层。核心思路就是:用预处理器宏区分平台,然后提供统一的接口。
// 跨平台线程封装示例
#ifdef _WIN32
#include <windows.h>
using ThreadHandle = HANDLE;
using ThreadFunc = DWORD (WINAPI*)(LPVOID);
#else
#include <pthread.h>
using ThreadHandle = pthread_t;
using ThreadFunc = void* (*)(void*);
#endif
class Thread {
public:
template <typename Func, typename... Args>
bool Start(Func&& func, Args&&... args) {
// 内部实现:根据平台调用CreateThread或pthread_create
// 这里省略具体实现细节
}
void Join() {
// 内部实现:根据平台调用WaitForSingleObject或pthread_join
}
private:
ThreadHandle handle_;
};
这种封装方式,我在一个嵌入式项目中用过。当时项目要同时支持Windows和Linux,但又不允许引入Boost这种重量级库。自己封装的好处是轻量、可控,坏处是——你得把同步原语(互斥锁、条件变量、信号量)也全封装一遍。嗯,工作量不小。
- 用
#ifdef _WIN32/#else区分平台 - 统一线程函数签名(比如都用
void* (*)(void*)) - 封装同步原语时注意语义一致性(比如Windows的
CriticalSection对应pthreads的mutex) - 考虑异常安全:确保线程创建失败时资源正确释放
使用Boost.Thread:成熟稳定的跨平台方案
Boost.Thread是C++11标准线程库的前身,也是很多老项目的首选。它的接口跟std::thread非常相似,但多了一些实用功能(比如thread_group、barrier)。
// Boost.Thread示例
#include <boost/thread.hpp>
#include <iostream>
void Worker(int id) {
std::cout << "线程 " << id << " 开始工作" << std::endl;
boost::this_thread::sleep(boost::posix_time::seconds(1));
std::cout << "线程 " << id << " 工作结束" << std::endl;
}
int main() {
boost::thread_group group;
for (int i = 0; i < 5; ++i) {
group.create_thread(boost::bind(Worker, i));
}
group.join_all(); // 等待所有线程结束
return 0;
}
Boost.Thread的thread_group是我特别喜欢的功能。你想想看,如果你要管理10个线程,用std::thread得手动维护一个vector<thread>,然后一个个join。而thread_group直接帮你搞定,一个join_all就完事了。
如果你在维护一个老项目,而且已经用了Boost的其他组件(比如Boost.Asio、Boost.Filesystem),那引入Boost.Thread是顺理成章的事。但如果是从零开始的新项目,我建议直接用C++11的
std::thread——毕竟标准库不需要额外依赖,而且现代编译器对C++11的支持已经很好了。
知识体系总览
下面这张图,是我自己总结的跨平台并发编程知识体系。你可以把它当作一张「地图」,遇到具体问题时知道该往哪个方向查。
如何选择?我的建议
说了这么多,到底该用哪个?我根据实际项目经验,给你几个参考标准:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 新项目,C++11及以上 | std::thread |
标准库,无依赖,现代编译器支持好 |
| 老项目,已用Boost | Boost.Thread | 与现有Boost组件无缝集成 |
| 嵌入式/资源受限 | 自己封装 | 轻量,可控,无额外依赖 |
| 需要高级功能(如barrier) | Boost.Thread | 标准库没有barrier,Boost有 |
| 跨平台移植已有代码 | 根据目标平台选择 | 尽量保持原有接口不变 |
不管你选哪种方案,一定要在项目初期就定好。我见过一个项目,Windows上用
std::thread,Linux上用pthreads,结果维护了两年后,代码里到处都是#ifdef,改一个bug要改两个地方。说白了,跨平台不是「能跑就行」,而是「好维护才行」。
好了,关于跨平台并发编程,今天就聊到这儿。记住:没有银弹,只有最适合你项目的方案。如果你还在犹豫,不妨从std::thread开始——毕竟它是标准,未来也不会过时。