7、线程与同步:POSIX线程(pthread)与Windows线程(CreateThread)的抽象封装
线程这东西,说白了就是让程序能同时干好几件事。我在做跨平台项目时,最头疼的就是线程API的差异。Linux上用pthread,Windows上用CreateThread,写法完全不同。你想想看,如果每个平台都写一套线程代码,维护起来得多痛苦?
所以我们需要一个抽象层。把线程的创建、销毁、同步这些操作统一封装起来。这样底层代码只需要写一次,上层调用完全不用关心你在哪个平台上跑。
7.1 线程创建与销毁的抽象
先看一个最简单的线程抽象接口。我个人习惯用结构体来封装线程句柄,这样扩展起来方便。
// thread.h — 跨平台线程抽象接口
#ifndef THREAD_H
#define THREAD_H
#ifdef _WIN32
#include <windows.h>
typedef HANDLE thread_t;
#else
#include <pthread.h>
typedef pthread_t thread_t;
#endif
// 线程入口函数类型
typedef void* (*thread_func_t)(void* arg);
// 创建线程
int thread_create(thread_t* t, thread_func_t func, void* arg);
// 等待线程结束
int thread_join(thread_t t, void** retval);
// 获取当前线程ID
thread_t thread_self(void);
#endif // THREAD_H
这里的关键是thread_t这个类型。在Windows下它是HANDLE,在Linux下它是pthread_t。上层代码根本不需要知道这些细节。
接下来是实现部分。嗯,这里要注意,Windows的线程入口函数签名和pthread不一样。Windows要求返回DWORD,pthread要求返回void*。所以我们需要一个适配层。
// thread.c — 跨平台线程实现
#include "thread.h"
#ifdef _WIN32
// Windows线程适配函数
typedef struct {
thread_func_t func;
void* arg;
} thread_wrapper_t;
static DWORD WINAPI thread_wrapper(LPVOID param) {
thread_wrapper_t* w = (thread_wrapper_t*)param;
void* ret = w->func(w->arg);
free(w);
return (DWORD)(intptr_t)ret;
}
int thread_create(thread_t* t, thread_func_t func, void* arg) {
thread_wrapper_t* w = malloc(sizeof(thread_wrapper_t));
if (!w) return -1;
w->func = func;
w->arg = arg;
*t = CreateThread(NULL, 0, thread_wrapper, w, 0, NULL);
return (*t != NULL) ? 0 : -1;
}
int thread_join(thread_t t, void** retval) {
DWORD ret = WaitForSingleObject(t, INFINITE);
if (ret == WAIT_OBJECT_0) {
if (retval) {
DWORD exit_code;
GetExitCodeThread(t, &exit_code);
*retval = (void*)(intptr_t)exit_code;
}
CloseHandle(t);
return 0;
}
return -1;
}
#else // POSIX
int thread_create(thread_t* t, thread_func_t func, void* arg) {
return pthread_create(t, NULL, func, arg);
}
int thread_join(thread_t t, void** retval) {
return pthread_join(t, retval);
}
thread_t thread_self(void) {
#ifdef _WIN32
return GetCurrentThread();
#else
return pthread_self();
#endif
}
#endif
7.2 互斥锁的抽象封装
线程同步是另一个大坑。我曾经在项目中遇到过死锁问题,查了两天才发现是Windows和Linux的互斥锁初始化方式不同导致的。
先看接口定义:
// mutex.h — 跨平台互斥锁接口
#ifndef MUTEX_H
#define MUTEX_H
#ifdef _WIN32
typedef CRITICAL_SECTION mutex_t;
#else
typedef pthread_mutex_t mutex_t;
#endif
int mutex_init(mutex_t* m);
int mutex_lock(mutex_t* m);
int mutex_unlock(mutex_t* m);
int mutex_destroy(mutex_t* m);
#endif
实现部分:
// mutex.c
#include "mutex.h"
int mutex_init(mutex_t* m) {
#ifdef _WIN32
InitializeCriticalSection(m);
return 0;
#else
return pthread_mutex_init(m, NULL);
#endif
}
int mutex_lock(mutex_t* m) {
#ifdef _WIN32
EnterCriticalSection(m);
return 0;
#else
return pthread_mutex_lock(m);
#endif
}
int mutex_unlock(mutex_t* m) {
#ifdef _WIN32
LeaveCriticalSection(m);
return 0;
#else
return pthread_mutex_unlock(m);
#endif
}
int mutex_destroy(mutex_t* m) {
#ifdef _WIN32
DeleteCriticalSection(m);
return 0;
#else
return pthread_mutex_destroy(m);
#endif
}
7.3 条件变量的抽象
条件变量在pthread和Windows下的实现差异更大。Windows Vista之后才原生支持条件变量,之前得用事件对象模拟。
// condvar.h — 跨平台条件变量
#ifndef CONDVAR_H
#define CONDVAR_H
#include "mutex.h"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT >= 0x0600
typedef CONDITION_VARIABLE condvar_t;
#else
// 老版本Windows用事件模拟
typedef struct {
HANDLE event;
int waiters;
CRITICAL_SECTION lock;
} condvar_t;
#endif
#else
typedef pthread_cond_t condvar_t;
#endif
int condvar_init(condvar_t* cv);
int condvar_wait(condvar_t* cv, mutex_t* m);
int condvar_signal(condvar_t* cv);
int condvar_broadcast(condvar_t* cv);
int condvar_destroy(condvar_t* cv);
#endif
实现时,我建议尽量用系统原生API。Windows Vista以上直接用CONDITION_VARIABLE,性能更好。如果是XP系统,那就得自己用事件对象模拟了。
7.4 读写锁的封装
读写锁在pthread里是pthread_rwlock_t,Windows下没有原生支持。我一般用互斥锁加计数器自己实现一个。
// rwlock.h
#ifndef RWLOCK_H
#define RWLOCK_H
#include "mutex.h"
typedef struct {
mutex_t lock;
int readers;
int writers;
} rwlock_t;
int rwlock_init(rwlock_t* rw);
int rwlock_rdlock(rwlock_t* rw);
int rwlock_wrlock(rwlock_t* rw);
int rwlock_unlock(rwlock_t* rw);
int rwlock_destroy(rwlock_t* rw);
#endif
实现时要注意写者优先的问题。我习惯用两个条件变量来实现,一个给读者用,一个给写者用。这样能避免写者饥饿。
7.5 线程池的简单封装
线程池是实际项目中常用的模式。我封装了一个简单的线程池,支持任务队列和动态调整线程数。
// threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H
#include "thread.h"
#include "mutex.h"
typedef struct task_s {
thread_func_t func;
void* arg;
struct task_s* next;
} task_t;
typedef struct {
thread_t* threads;
int thread_count;
task_t* task_queue;
task_t* task_tail;
mutex_t queue_lock;
int shutdown;
int active_tasks;
} threadpool_t;
int threadpool_create(threadpool_t* pool, int num_threads);
int threadpool_submit(threadpool_t* pool, thread_func_t func, void* arg);
int threadpool_destroy(threadpool_t* pool);
#endif
7.6 知识体系图
下面这张图展示了线程抽象封装的整体架构:
7.7 实际项目中的避坑指南
我曾经在做一个网络服务器项目时,遇到过一个诡异的崩溃问题。排查了很久才发现是Windows下线程栈空间默认只有1MB,而Linux默认是8MB。我们的工作线程需要处理大块数据,栈溢出了。
解决方案是在创建线程时指定栈大小:
// 在抽象接口中增加栈大小参数
int thread_create_ex(thread_t* t, thread_func_t func, void* arg, size_t stack_size);
// Windows实现
#ifdef _WIN32
*t = CreateThread(NULL, stack_size, thread_wrapper, w, 0, NULL);
#else
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, stack_size);
pthread_create(t, &attr, func, arg);
pthread_attr_destroy(&attr);
#endif
另一个常见问题是线程命名。在Linux下可以用pthread_setname_np,Windows下用SetThreadDescription。调试时能看到线程名字,能省不少事。
thread_set_name函数,内部用条件编译分别调用不同平台的API。这样调试时一眼就能看出哪个线程在干什么。
7.8 性能对比与选择建议
| 特性 | POSIX (pthread) | Windows |
|---|---|---|
| 线程创建速度 | 较快(用户态) | 较慢(内核态) |
| 互斥锁性能 | 优秀(futex) | 优秀(CRITICAL_SECTION) |
| 条件变量 | 原生支持 | Vista+ 原生支持 |
| 读写锁 | 原生支持 | 需要自己实现 |
| 线程局部存储 | pthread_key_t | TlsAlloc / __declspec(thread) |
从性能上看,pthread在Linux上更轻量,因为它是用户态线程库。Windows的线程是内核对象,创建和销毁开销更大。所以如果你在Windows上需要大量短生命周期线程,建议用线程池来复用。
嗯,最后说一句。抽象封装不是银弹。有些平台特有的功能,比如Windows的I/O完成端口(IOCP),或者Linux的epoll,这些高性能特性很难完全抽象。我的做法是:80%的通用逻辑用抽象层,20%的性能关键路径直接暴露平台API。这样既保证了可移植性,又不牺牲性能。
公众号:蓝海资料掘金营,微信deep3321