第1章:C11线程库入门——从thrd_create说起

说实话,我第一次接触C11线程库时,心里是有点抵触的。那时候我已经用POSIX线程写了五六年代码,习惯了pthread_create那一套。但后来在一个跨平台项目里,我被坑惨了——Windows上pthread模拟层跑出来的bug,查了整整三天。从那以后,我开始认真研究C11标准线程库。

嗯,今天我们就从最基础的东西聊起。C11标准给C语言带来了原生的线程支持,说白了就是不用再依赖操作系统特定的API了。你写一份代码,在Linux、Windows、macOS上都能编译通过,这才是真正的跨平台。

1.1 thrd_create:线程的起点

创建线程,是所有多线程编程的第一步。C11里用的是thrd_create,它的签名长这样:

#include <threads.h>

int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);

参数很简单:

  • thr:指向线程标识符的指针,相当于POSIX里的pthread_t
  • func:线程入口函数,必须是 int (*)(void*) 这种形式
  • arg:传给线程函数的参数

返回值是 thrd_success 或错误码。我个人习惯是创建完立刻检查返回值,别偷懒。

核心要点:线程函数返回 thrd_success(0)表示正常退出,返回其他值表示异常终止。主线程可以用 thrd_join 等待子线程结束。

来看个最简单的例子:

#include <stdio.h>
#include <threads.h>

int worker(void *arg) {
    int id = *(int*)arg;
    printf("线程 %d 开始干活\n", id);
    return thrd_success;
}

int main(void) {
    thrd_t t;
    int id = 42;

    if (thrd_create(&t, worker, &id) != thrd_success) {
        fprintf(stderr, "创建线程失败\n");
        return 1;
    }

    thrd_join(t, NULL);
    printf("主线程结束\n");
    return 0;
}

你想想看,这段代码在Linux上用gcc编译,在Windows上用MSVC编译,都能跑。这就是C11线程库的价值所在。

小技巧:我建议把线程参数用结构体封装起来。比如传多个参数时,定义一个struct,把指针传进去。这样代码清晰,也方便扩展。

1.2 mtx_t:互斥锁,保护共享数据

多线程编程里,最头疼的就是数据竞争。两个线程同时写一个变量,结果不可预测。这时候就需要互斥锁——mtx_t

它的用法和POSIX的pthread_mutex_t几乎一样:

mtx_t mutex;
mtx_init(&mutex, mtx_plain);  // 普通互斥锁
mtx_lock(&mutex);
// 临界区代码
mtx_unlock(&mutex);
mtx_destroy(&mutex);

第二个参数可以指定锁的类型:

  • mtx_plain:普通锁,不支持递归
  • mtx_recursive:递归锁,同一个线程可以多次加锁
  • mtx_timed:支持超时的锁

我曾经在一个项目中,因为用了递归锁没注意,导致死锁。排查了半天才发现是锁的嵌套层级太深。嗯,这里要注意:能用普通锁就别用递归锁,它掩盖了设计问题。

避坑指南:我曾经在嵌入式项目里,忘记调用 mtx_destroy,结果内存泄漏。虽然线程结束后锁会自动释放,但资源回收一定要养成习惯。

1.3 cnd_t:条件变量,线程间的信号灯

条件变量用来让线程等待某个条件成立。比如生产者-消费者模型里,消费者等数据,生产者通知数据来了。

C11的条件变量是 cnd_t,用法也很直观:

cnd_t cond;
cnd_init(&cond);

// 等待条件
cnd_wait(&cond, &mutex);

// 唤醒一个等待线程
cnd_signal(&cond);

// 唤醒所有等待线程
cnd_broadcast(&cond);

cnd_destroy(&cond);

注意:cnd_wait 必须在持有互斥锁的情况下调用。它会原子性地释放锁并进入等待,被唤醒后重新获取锁。

来看一个完整的例子:

#include <stdio.h>
#include <threads.h>

mtx_t mutex;
cnd_t cond;
int data_ready = 0;

int producer(void *arg) {
    mtx_lock(&mutex);
    data_ready = 1;
    printf("生产者:数据准备好了\n");
    cnd_signal(&cond);
    mtx_unlock(&mutex);
    return thrd_success;
}

int consumer(void *arg) {
    mtx_lock(&mutex);
    while (!data_ready) {
        cnd_wait(&cond, &mutex);
    }
    printf("消费者:收到数据\n");
    mtx_unlock(&mutex);
    return thrd_success;
}

int main(void) {
    thrd_t p, c;
    mtx_init(&mutex, mtx_plain);
    cnd_init(&cond);

    thrd_create(&c, consumer, NULL);
    thrd_create(&p, producer, NULL);

    thrd_join(p, NULL);
    thrd_join(c, NULL);

    cnd_destroy(&cond);
    mtx_destroy(&mutex);
    return 0;
}

关键点:条件变量等待一定要用 while 循环检查条件,而不是 if。因为存在「虚假唤醒」——线程可能在没有收到信号的情况下被唤醒。这是POSIX和C11都有的行为,别问我怎么知道的,踩过坑。

1.4 C11线程库 vs POSIX线程:一张表说清楚

很多从POSIX转过来的朋友,会觉得C11的API名字别扭。我刚开始也这么觉得,但用顺手了发现其实更简洁。来看对比:

功能 POSIX (pthread) C11 (threads.h)
创建线程 pthread_create thrd_create
等待线程 pthread_join thrd_join
互斥锁 pthread_mutex_t mtx_t
条件变量 pthread_cond_t cnd_t
线程本地存储 pthread_key_t tss_t
一次性初始化 pthread_once call_once

说白了,C11就是给这些功能换了个马甲,但核心语义完全一致。如果你熟悉POSIX,学C11线程库大概只需要半小时。

1.5 跨平台线程编程的实战心得

我参与过一个需要同时部署在Linux服务器和Windows工控机的项目。一开始用的POSIX线程,Windows上靠pthreads-w32模拟。结果呢?

  • Windows上的pthread模拟层性能差,锁竞争激烈时吞吐量掉了一半
  • 某些pthread扩展函数(如pthread_spinlock)在模拟层里是空的
  • 调试时堆栈信息对不上,排查问题像大海捞针

后来全部改成C11线程库,代码干净了,问题也少了。虽然C11线程库功能比POSIX少一些(比如没有读写锁、屏障),但应付80%的场景足够了。

我的建议:如果你写的是纯C项目,并且需要跨平台,优先用C11线程库。如果必须用高级特性(如屏障、自旋锁),再考虑封装一层POSIX。

1.6 本章知识体系

下面这张图帮你理清C11线程库的核心脉络:

C11线程库核心组件 thrd_create 线程创建与管理 mtx_t 互斥锁与同步 cnd_t 条件变量与通知 thrd_join / thrd_detach mtx_lock / mtx_unlock cnd_wait / cnd_signal 跨平台:同一份代码,Linux/Windows/macOS 均可编译运行 与POSIX语义兼容,但API更简洁 注意:C11线程库不支持读写锁、屏障等高级特性

这张图把C11线程库的三个核心组件串起来了。你写代码时,基本就是这三板斧:创建线程、加锁保护数据、用条件变量做同步。

总结一下:C11线程库虽然功能不如POSIX丰富,但胜在跨平台和标准化。对于大多数应用场景,thrd_create + mtx_t + cnd_t 已经足够。记住:能用标准库就别自己造轮子,这是我在项目里用血泪换来的经验。


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