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_SECTIONMutex 内核对象
  • 条件变量:Windows Vista 之后才有 CONDITION_VARIABLE

嗯,这里要注意:Windows的CRITICAL_SECTION是用户态轻量锁,性能比内核态的Mutex好很多。我个人习惯在Windows上优先用CRITICAL_SECTION

7.2 抽象层设计思路

我的做法是定义一套中间层接口,底层用条件编译分别实现。核心思路就一句话:把平台差异藏在宏和函数指针后面

下面这张图展示了整体架构:

线程抽象层架构 应用代码(调用统一接口) 抽象接口层(thrd_t, mtx_t, cnd_t) POSIX 实现(pthread) Windows 实现(Win32 API) 通过 #ifdef _WIN32 / #else 条件编译切换

7.3 核心数据结构定义

我们先定义一套跨平台的类型。我个人习惯用thrd_tmtx_tcnd_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
}
注意:Windows上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
}
小技巧:Windows的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(&not_full, &mtx);
        }
        
        buffer[count++] = i;
        printf("Produced: %d\n", i);
        
        cnd_signal(&not_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(&not_empty, &mtx);
        }
        
        int val = buffer[--count];
        printf("Consumed: %d\n", val);
        
        cnd_signal(&not_full);
        mtx_unlock(&mtx);
    }
    return NULL;
}

int main() {
    mtx_init(&mtx);
    cnd_init(&not_full);
    cnd_init(&not_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(&not_full);
    cnd_destroy(&not_empty);
    
    return 0;
}

这段代码在Linux和Windows上都能编译通过,行为完全一致。嗯,这就是抽象封装的价值——你只需要写一次业务逻辑,平台差异由底层代码去扛。

我曾经在一个项目里用这套封装写了整个线程池,后来从ARM Linux移植到x64 Windows,只改了编译选项,一行业务代码都没动。那种感觉,真的很爽。

最后提醒一句:跨平台封装不是银弹。有些平台特有的功能(比如Windows的纤程、Linux的clone系统调用)是封装不了的。遇到这种情况,我建议用#ifdef单独暴露平台扩展接口,不要强行塞进统一抽象层里。


公众号:蓝海资料掘金营,微信deep3321