131 / 163 · C11 · 约 8 分钟
插曲:线程 API
本插曲概述 POSIX 线程库的核心调用,用于启动新执行流、等待它们结束以及用互斥锁保护共享数据。接口设计兼顾易用性与灵活性,后续章节会用大量例子展开锁与条件变量。
创建线程
要获得额外的并发执行流,程序调用 pthread_create。调用者提供一块 pthread_t 存储新线程标识、可选属性指针(通常传 NULL 使用默认栈大小与调度策略)、一个函数指针指出线程从何处开始运行,以及一个 void* 参数。起始函数签名必须是 void*(*)(void*),因此任何类型的数据都可通过强制转换或打包进结构体传入。创建成功后,新线程拥有独立调用栈,却与原线程共享同一地址空间。
等待线程结束
仅仅创建线程往往不够;调用者通常需要知道工作何时完成并收集结果。pthread_join 阻塞当前线程直到指定 pthread_t 对应的线程退出,并可把该线程的返回值写入一个 void**。若不关心返回值,第二个参数可传 NULL。长时间运行的服务器可能永远不 join,但计算型并行程序几乎总是 join,以保证所有工作在进入下一阶段或退出前全部结束。
互斥锁
多个线程同时读写同一变量会产生数据竞争。POSIX 提供 pthread_mutex_t 来实现互斥。锁可静态用 PTHREAD_MUTEX_INITIALIZER 初始化,也可动态用 pthread_mutex_init。进入临界区前调用 pthread_mutex_lock,离开后调用 pthread_mutex_unlock。同一时刻只有一个线程能持有该锁,从而把对共享状态的修改串行化。
参数与返回值的安全传递
void* 既可传入也可传出任意类型。单个整数可直接强制转换;多个值则定义结构体并传递其地址。若线程需要把结果交还给 join 者,该结构体必须用 malloc 分配在堆上。绝不能返回指向线程栈上局部变量的指针,因为函数一返回栈帧即被回收,指针立刻失效。
常见误区
- 从线程函数返回栈上局部变量的地址会造成悬挂指针和未定义行为。
- 创建线程后不 join 可能让主线程过早退出,工作线程被强制杀掉。
- 未持有对应互斥锁就访问共享变量会产生数据竞争。
- 对未初始化或已销毁的互斥锁调用 lock/unlock 会导致崩溃或死锁。
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
#include <pthread.h>
#define N 1000
int counter = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *worker(void *arg) { (void)arg;
int i;
for (i = 0; i < N; i++) {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, worker, NULL);
pthread_create(&t2, NULL, worker, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final counter: %d\n", counter);
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -pthread ostep-27-thread-api.c -o example && ./example预期结果
Final counter: 2000
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
线程函数在自己的栈上分配一个结构体并返回其地址,主线程在 pthread_join 之后解引用该指针会怎样?原因是什么?
查看参考答案
读到的是已释放内存,结果是垃圾数据或崩溃。线程函数返回时其栈帧被回收,指针立即失效。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。