7、线程与同步:POSIX threads (pthread) 与 Windows Threads 的抽象与封装
说到跨平台开发,线程这块儿绝对是绕不开的硬骨头。我早年做嵌入式Linux转Windows移植时,就被pthread和Windows Threads的差异坑过好几次。说白了,两个平台的线程API长得完全不一样,但干的活儿是一样的——创建线程、等待结束、加锁同步。
这一章,我们就来聊聊怎么把这两套东西抽象成一套统一的接口。你想想看,如果能在Linux上写一套代码,拿到Windows上直接编译就能跑,那该多省心。
7.1 两套API的差异在哪里
先看看它们各自长什么样。
POSIX threads (pthread):
- 头文件:
<pthread.h> - 线程创建:
pthread_create() - 线程等待:
pthread_join() - 互斥锁:
pthread_mutex_t+pthread_mutex_lock/unlock - 条件变量:
pthread_cond_t
Windows Threads:
- 头文件:
<windows.h> - 线程创建:
CreateThread() - 线程等待:
WaitForSingleObject()/WaitForMultipleObjects() - 互斥锁:
CRITICAL_SECTION或Mutex内核对象 - 条件变量:Windows Vista 之后才有
CONDITION_VARIABLE
嗯,这里要注意:Windows的CRITICAL_SECTION是用户态轻量锁,性能比内核态的Mutex好很多。我个人习惯在Windows上优先用CRITICAL_SECTION。
7.2 抽象层设计思路
我的做法是定义一套中间层接口,底层用条件编译分别实现。核心思路就一句话:把平台差异藏在宏和函数指针后面。
下面这张图展示了整体架构:
7.3 核心数据结构定义
我们先定义一套跨平台的类型。我个人习惯用thrd_t、mtx_t、cnd_t这样的命名,跟C11标准线程库保持一致。
// thrd_platform.h
#ifndef THRD_PLATFORM_H
#define THRD_PLATFORM_H
#ifdef _WIN32
#include <windows.h>
typedef HANDLE thrd_t;
typedef CRITICAL_SECTION mtx_t;
typedef CONDITION_VARIABLE cnd_t;
#else
#include <pthread.h>
typedef pthread_t thrd_t;
typedef pthread_mutex_t mtx_t;
typedef pthread_cond_t cnd_t;
#endif
#endif // THRD_PLATFORM_H
你看,这样一封装,上层代码根本不用关心底层是什么。我在项目中遇到过有人试图用void*来统一所有平台类型,结果类型转换搞得一团糟。不如直接用typedef,编译器还能帮你做类型检查。
7.4 线程创建与等待
接下来是线程的创建和等待。这里有个坑:Windows的CreateThread要求线程函数返回DWORD,而pthread要求返回void*。怎么统一?
我的做法是定义一个中间函数做适配:
// thrd_create.c
#include "thrd_platform.h"
#ifdef _WIN32
// Windows线程函数适配器
typedef DWORD (WINAPI *win_thread_func)(LPVOID);
int thrd_create(thrd_t *thr, void *(*func)(void*), void *arg) {
// 包装一下,把 void* 返回值转成 DWORD
struct wrapper {
void *(*f)(void*);
void *a;
};
wrapper *w = new wrapper{func, arg};
*thr = CreateThread(NULL, 0,
[](LPVOID p) -> DWORD {
wrapper *w = (wrapper*)p;
w->f(w->a);
delete w;
return 0;
}, w, 0, NULL);
return (*thr != NULL) ? 0 : -1;
}
#else
int thrd_create(thrd_t *thr, void *(*func)(void*), void *arg) {
return pthread_create(thr, NULL, func, arg);
}
#endif
int thrd_join(thrd_t thr, void **retval) {
#ifdef _WIN32
WaitForSingleObject(thr, INFINITE);
if (retval) *retval = NULL;
CloseHandle(thr);
return 0;
#else
return pthread_join(thr, retval);
#endif
}
CreateThread创建的线程句柄必须用CloseHandle关闭,否则会句柄泄漏。我早期移植代码时就漏了这个,跑了一周后程序莫名其妙崩溃,查了半天才发现是句柄用光了。
7.5 互斥锁的封装
互斥锁这块相对简单,因为两个平台的语义基本一致。唯一要注意的是初始化方式不同。
// mtx.c
#include "thrd_platform.h"
int mtx_init(mtx_t *mtx) {
#ifdef _WIN32
InitializeCriticalSection(mtx);
return 0;
#else
return pthread_mutex_init(mtx, NULL);
#endif
}
int mtx_lock(mtx_t *mtx) {
#ifdef _WIN32
EnterCriticalSection(mtx);
return 0;
#else
return pthread_mutex_lock(mtx);
#endif
}
int mtx_unlock(mtx_t *mtx) {
#ifdef _WIN32
LeaveCriticalSection(mtx);
return 0;
#else
return pthread_mutex_unlock(mtx);
#endif
}
int mtx_destroy(mtx_t *mtx) {
#ifdef _WIN32
DeleteCriticalSection(mtx);
return 0;
#else
return pthread_mutex_destroy(mtx);
#endif
}
CRITICAL_SECTION支持递归进入(同一线程重复加锁),而pthread的普通互斥锁默认不支持。如果你需要递归锁,pthread那边要设置PTHREAD_MUTEX_RECURSIVE属性。我建议在抽象层统一使用递归锁,避免上层逻辑不小心重入时死锁。
7.6 条件变量的实现
条件变量是跨平台封装里最麻烦的部分。Windows在Vista之前根本没有条件变量,只能用事件对象(Event)模拟。好在现在Windows 7+都支持CONDITION_VARIABLE了,我们直接用它。
// cnd.c
#include "thrd_platform.h"
int cnd_init(cnd_t *cond) {
#ifdef _WIN32
InitializeConditionVariable(cond);
return 0;
#else
return pthread_cond_init(cond, NULL);
#endif
}
int cnd_wait(cnd_t *cond, mtx_t *mtx) {
#ifdef _WIN32
return SleepConditionVariableCS(cond, mtx, INFINITE) ? 0 : -1;
#else
return pthread_cond_wait(cond, mtx);
#endif
}
int cnd_signal(cnd_t *cond) {
#ifdef _WIN32
WakeConditionVariable(cond);
return 0;
#else
return pthread_cond_signal(cond);
#endif
}
int cnd_broadcast(cnd_t *cond) {
#ifdef _WIN32
WakeAllConditionVariable(cond);
return 0;
#else
return pthread_cond_broadcast(cond);
#endif
}
int cnd_destroy(cnd_t *cond) {
#ifdef _WIN32
// Windows条件变量不需要显式销毁
return 0;
#else
return pthread_cond_destroy(cond);
#endif
}
关键点:Windows的SleepConditionVariableCS要求传入的互斥锁必须是CRITICAL_SECTION类型。这就是为什么我们之前把mtx_t定义成CRITICAL_SECTION,而不是Windows内核Mutex对象。两者不能混用,切记!
7.7 完整示例:生产者-消费者
最后,我们用一个经典的生产者-消费者例子来验证这套抽象层好不好用。
// demo.c
#include "thrd_platform.h"
#include <stdio.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;
mtx_t mtx;
cnd_t not_full;
cnd_t not_empty;
void* producer(void *arg) {
for (int i = 0; i < 20; i++) {
mtx_lock(&mtx);
while (count == BUFFER_SIZE) {
cnd_wait(¬_full, &mtx);
}
buffer[count++] = i;
printf("Produced: %d\n", i);
cnd_signal(¬_empty);
mtx_unlock(&mtx);
}
return NULL;
}
void* consumer(void *arg) {
for (int i = 0; i < 20; i++) {
mtx_lock(&mtx);
while (count == 0) {
cnd_wait(¬_empty, &mtx);
}
int val = buffer[--count];
printf("Consumed: %d\n", val);
cnd_signal(¬_full);
mtx_unlock(&mtx);
}
return NULL;
}
int main() {
mtx_init(&mtx);
cnd_init(¬_full);
cnd_init(¬_empty);
thrd_t prod, cons;
thrd_create(&prod, producer, NULL);
thrd_create(&cons, consumer, NULL);
thrd_join(prod, NULL);
thrd_join(cons, NULL);
mtx_destroy(&mtx);
cnd_destroy(¬_full);
cnd_destroy(¬_empty);
return 0;
}
这段代码在Linux和Windows上都能编译通过,行为完全一致。嗯,这就是抽象封装的价值——你只需要写一次业务逻辑,平台差异由底层代码去扛。
我曾经在一个项目里用这套封装写了整个线程池,后来从ARM Linux移植到x64 Windows,只改了编译选项,一行业务代码都没动。那种感觉,真的很爽。
最后提醒一句:跨平台封装不是银弹。有些平台特有的功能(比如Windows的纤程、Linux的clone系统调用)是封装不了的。遇到这种情况,我建议用#ifdef单独暴露平台扩展接口,不要强行塞进统一抽象层里。
公众号:蓝海资料掘金营,微信deep3321