Barrier 栅栏同步:为什么“人齐了再开桌”是一个线程同步模型
有些并发问题不是“谁先进门”,而是“所有人都到齐以后,大家才能一起往下走”。这类问题,用 Barrier 会比单纯加锁更自然。
先从一个火锅局说起
假设 4 个朋友约好吃火锅。规则很简单:谁先到店都不能先点火开吃,必须在门口等;等第 4 个人也到了,大家一起进包间。
这就是 Barrier 的直觉模型:先到的人停在同一个关口,最后一个人到达时,关口打开,所有人继续。
在多线程里,很多问题也不是“抢一个资源”,而是“多个线程必须在某个阶段对齐”:
一句话:Barrier 不是保护一份数据,而是保护一个“阶段边界”。
Barrier 到底是什么
Barrier 可以翻译成“栅栏”或“屏障”。它内部有一个固定的参与者数量 count。每个线程调用 pthread_barrier_wait() 时,都会表示“我到了”。
当到达线程数还没凑够 count:当前线程阻塞等待。
当最后一个线程也到达:Barrier 打开,所有等待线程一起返回。
这个模型有 3 个关键点:
pthread_barrier 常用 API
POSIX 里常见的 Barrier API 主要是下面几类。
| | | |
|---|
pthread_barrier_init() | | | count |
pthread_barrier_wait() | | | |
pthread_barrier_destroy() | | | |
pthread_barrierattr_init() | | | |
pthread_barrierattr_setpshared() | | PTHREAD_PROCESS_PRIVATE | |
pthread_barrierattr_destroy() | | | |
生命周期可以记成一条很短的线:先建门、再等人、最后拆门。
pthread_barrier_init
函数原型如下:
intpthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned count);
pthread_barrier_wait
函数原型如下:
intpthread_barrier_wait(pthread_barrier_t *barrier);
返回值有一个很有用的细节:
| | |
|---|
0 | | |
PTHREAD_BARRIER_SERIAL_THREAD | | |
| | |
PTHREAD_BARRIER_SERIAL_THREAD 不是“主线程”,也不是固定线程。它只表示:这一轮 Barrier 放行时,系统挑了一个线程,让它有机会做只需要执行一次的事情。
pthread_barrier_destroy
函数原型如下:
intpthread_barrier_destroy(pthread_barrier_t *barrier);
它的语义是“销毁不再使用的 Barrier 资源”。如果还有线程阻塞在 pthread_barrier_wait(),就不应该销毁它。
这里特别容易误会:destroy 不负责唤醒,也不是取消等待。 如果你真正要表达的是“后台线程准备好了 / 失败了 / 超时了”,通常应该用 condition variable + state,而不是 Barrier。
一个最小例子:多线程同时起跑
下面这个例子里,主线程创建 4 个 worker。每个 worker 先做自己的准备工作,然后卡在同一个 Barrier。等 4 个 worker 都准备好,它们再一起进入“正式执行”阶段。
#include<errno.h>#include<pthread.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>#define WORKER_COUNT 4static pthread_barrier_t start_barrier;staticvoid *worker_thread(void *arg){ long id = (long)arg; printf("worker %ld: prepare\n", id); usleep((useconds_t)(1000 * 100 * id)); printf("worker %ld: wait at barrier\n", id); int ret = pthread_barrier_wait(&start_barrier); if (ret != 0 && ret != PTHREAD_BARRIER_SERIAL_THREAD) { fprintf(stderr, "worker %ld: barrier_wait failed: %d\n", id, ret); return NULL; } if (ret == PTHREAD_BARRIER_SERIAL_THREAD) { printf("worker %ld: all workers are ready\n", id); } printf("worker %ld: start work\n", id); return NULL;}intmain(void){ pthread_t threads[WORKER_COUNT]; int ret = pthread_barrier_init(&start_barrier, NULL, WORKER_COUNT); if (ret != 0) { fprintf(stderr, "pthread_barrier_init failed: %d\n", ret); return 1; } for (long i = 0; i < WORKER_COUNT; ++i) { ret = pthread_create(&threads[i], NULL, worker_thread, (void *)i); if (ret != 0) { fprintf(stderr, "pthread_create failed: %d\n", ret); return 1; } } for (int i = 0; i < WORKER_COUNT; ++i) { pthread_join(threads[i], NULL); } ret = pthread_barrier_destroy(&start_barrier); if (ret != 0) { fprintf(stderr, "pthread_barrier_destroy failed: %d\n", ret); return 1; } return 0;}
如果你把它想成火锅局,代码里的角色就很好记:
| | |
|---|
pthread_barrier_t | | |
WORKER_COUNT | | |
pthread_barrier_wait() | | |
PTHREAD_BARRIER_SERIAL_THREAD | | |
pthread_barrier_destroy() | | |
Barrier 最适合的场景:分阶段并行
Barrier 的高光时刻,是“阶段 1 所有人都干完,再进阶段 2;阶段 2 所有人都干完,再进阶段 3”。
这种模式常见于下面几类任务:
| | |
|---|
| 并发压测同时起跑 | | |
| 分块计算 | | |
| 批处理流水线校验 | | |
| 复现竞态问题 | | |
这里要强调一点:Barrier 经常用在测试里,而且特别适合复现多线程问题。因为很多竞态 bug 平时不容易出现,本质是“刚好多个线程同时进入危险区域”。Barrier 可以把“刚好”变成“尽量同时”。
Barrier 不适合什么
Barrier 最大的问题也来自它的优点:它太强调“固定人数全部到齐”。一旦参与线程数不稳定,或者某个线程可能失败退出,它就容易把其他线程永远卡住。
按问题类型可以这样选:
| | |
|---|
| pthread_barrier_t | |
| pthread_cond_t + state | 可以表达 ready / failed / timeout |
| pthread_cond_t + mutex | |
| sem_t | |
| pthread_once() | |
常见坑:大多数都和“人数”有关
| | |
|---|
count | | 参与 wait() 的线程数必须和 count 一致 |
| | |
| | |
| | |
| | 保护共享数据仍然要用 mutex/atomic 等工具 |
一句话总结
Barrier 的心智模型非常简单:先到的人在门口等,最后一个人到达后,所有人一起进入下一阶段。
真正用好它,要记住三句话:
| |
|---|
| 等人数,用 Barrier | |
| 等条件,用 Cond | ready、failed、timeout 这类状态变化不要硬套 Barrier |
| 改共享数据,还是要加锁 | Barrier 不替代 mutex,它只负责阶段对齐 |
如果你在调试多线程问题,想让一批线程“同时冲进某段代码”,Barrier 往往是最小、最直观、也最容易解释的工具。