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
小提示:Windows下CreateThread的返回值是HANDLE,记得用CloseHandle释放。pthread的线程句柄是轻量级的,不需要手动释放。这个差异很容易踩坑。

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
}
注意:Windows的CRITICAL_SECTION是轻量级的,只能用于同一进程内的线程同步。pthread的互斥锁可以通过属性设置支持跨进程同步。如果你需要跨进程同步,Windows下得用Mutex对象(CreateMutex),而不是CRITICAL_SECTION。

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
核心思想:抽象封装的核心是「接口统一,实现分离」。上层代码只依赖我们定义的接口,底层实现根据平台选择不同的API。这样当你要移植到新平台时,只需要实现一套新的底层代码,上层完全不用动。

7.6 知识体系图

下面这张图展示了线程抽象封装的整体架构:

线程抽象封装架构 应用代码(不依赖平台) 抽象接口层 thread_create | mutex_lock | condvar_wait | rwlock_rdlock POSIX 实现 pthread_create pthread_mutex_lock / pthread_cond_wait Windows 实现 CreateThread / CloseHandle EnterCriticalSection / CONDITION_VARIABLE Linux / macOS / FreeBSD Windows 7/10/11 通过预编译宏 _WIN32 自动选择实现,上层代码零修改

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