barrier(屏障)是一种同步原语,主要用于多线程或多进程编程中,它的核心作用是:强制所有到达该点的线程必须等待,直到所有参与同步的线程都到达该点之后,这些线程才能继续向下执行。

你可以把它想象成一个“起跑线”或者“集合点”:
- 等待:线程 A、B、C 都在执行任务,当线程 A 第一个到达
barrier时,它会在那里停下来,等待其他伙伴。 - 汇聚:线程 B 和 C 也陆续到达
barrier,并停下来等待。 - 释放:当最后一个线程(比如线程 C)到达
barrier时,屏障“打开”,所有等待的线程(A、B、C)被同时“唤醒”,并可以继续执行barrier之后的代码。
为什么需要 Barrier?
Barrier 在并行计算中非常有用,尤其是在以下场景:
-
分阶段计算:一个大的计算任务被分成多个阶段,每个阶段可以并行执行,但下一个阶段的开始必须等待所有线程都完成当前阶段的计算。
- 例子:模拟一个物理系统,第一阶段,所有线程并行计算各自区域的力;第二阶段,所有线程需要基于所有区域的力来更新位置,那么在进入第二阶段前,必须有一个
barrier,确保所有力都计算完毕。
- 例子:模拟一个物理系统,第一阶段,所有线程并行计算各自区域的力;第二阶段,所有线程需要基于所有区域的力来更新位置,那么在进入第二阶段前,必须有一个
-
数据同步点:多个线程在并行处理数据,但在某个点,所有线程都需要访问一份共享的、完整的、一致的数据。
barrier可以确保所有线程都完成了数据的生成或预处理,然后再开始使用这份共享数据。
(图片来源网络,侵删) -
确保线程“同频”:防止某些线程因为执行速度快而“跑得太快”,导致它们访问了尚未被其他线程准备好的数据,从而引发数据竞争或不一致。
C 语言中的 Barrier 实现
在 C 语言中,barrier 并不像 mutex(互斥锁)那样是语言内置的关键字,它通常通过操作系统提供的 API 来实现,主要有两种方式:
POSIX 线程 (pthread) - 最常用
在 POSIX 线程库中,barrier 通过 pthread_barrier 相关函数来实现。
核心数据结构
#include <pthread.h> // 屏障对象 pthread_barrier_t barrier;
核心函数
-
int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count);
(图片来源网络,侵删)- 作用:初始化一个屏障。
barrier: 指向要初始化的屏障对象的指针。attr: 屏障属性,通常设为NULL使用默认属性。count: 必须等待的线程总数,这是一个关键值,一旦设定,通常不能更改。
-
int pthread_barrier_wait(pthread_barrier_t *barrier);- 作用:线程调用此函数来等待屏障。
- 当线程调用此函数时,它会阻塞,直到
count个线程都调用了pthread_barrier_wait。 - 返回值:
- 如果调用此函数的线程是最后一个到达屏障的线程,它会返回
PTHREAD_BARRIER_SERIAL_THREAD。 - 所有其他线程在屏障被释放后返回
0。
- 如果调用此函数的线程是最后一个到达屏障的线程,它会返回
- 重要:最后一个返回的线程有责任去销毁屏障(如果不再需要的话)。
-
int pthread_barrier_destroy(pthread_barrier_t *barrier);- 作用:销毁一个屏障对象,释放其占用的资源。
- 注意:只有在所有线程都已经通过屏障之后才能调用此函数。
示例代码
下面是一个经典的例子:多个线程并行打印,但必须等待所有线程都准备好后,才开始“打印,模拟一个同步事件。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 5
#define WORK_UNITS 10
// 屏障对象
pthread_barrier_t barrier;
// 线程工作函数
void* thread_work(void* arg) {
int thread_id = *(int*)arg;
for (int i = 0; i < WORK_UNITS; i++) {
// 模拟一些工作
// printf("Thread %d is working on unit %d\n", thread_id, i);
}
// 到达屏障,等待所有其他线程完成当前工作单元
printf("Thread %d has finished its work and is waiting at the barrier.\n", thread_id);
int result = pthread_barrier_wait(&barrier);
if (result == PTHREAD_BARRIER_SERIAL_THREAD) {
printf(">>> All threads have arrived! This is the last thread (%d). <<<\n", thread_id);
} else {
printf("Thread %d is proceeding after the barrier.\n", thread_id);
}
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_args[NUM_THREADS];
// 初始化屏障,等待 5 个线程
if (pthread_barrier_init(&barrier, NULL, NUM_THREADS) != 0) {
perror("Failed to initialize barrier");
return 1;
}
// 创建线程
for (int i = 0; i < NUM_THREADS; i++) {
thread_args[i] = i;
if (pthread_create(&threads[i], NULL, thread_work, &thread_args[i]) != 0) {
perror("Failed to create thread");
return 1;
}
}
// 等待所有线程完成
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// 销毁屏障
pthread_barrier_destroy(&barrier);
printf("All threads have finished.\n");
return 0;
}
执行结果分析: 你会看到,所有线程都会先打印 "has finished its work and is waiting at the barrier.",然后程序会暂停,直到第 5 个线程也打印出这句话后,所有线程才会被“唤醒”,其中一个线程(通常是最后一个到达的)会打印 ">>> All threads have arrived! ... <<<",然后所有线程继续执行并打印 "is proceeding after the barrier."。
Windows API (Windows.h)
如果你在 Windows 平台上进行开发,可以使用 Windows 提供的同步机制。
核心数据结构
#include <windows.h> // 事件对象,用于实现屏障 HANDLE hBarrierEvent; // 临界区和计数器,用于跟踪已到达的线程 CRITICAL_SECTION cs; int arrived_count = 0; const int total_threads = 5;
实现逻辑
Windows 没有直接的 barrier API,但可以用 WaitForSingleObject 和 SetEvent 来模拟。
-
初始化:
hBarrierEvent = CreateEvent(NULL, TRUE, TRUE, NULL);// 创建一个手动重置的事件,初始状态为已触发。InitializeCriticalSection(&cs);
-
等待函数:
- 线程进入临界区,增加
arrived_count。 arrived_count == total_threads,说明自己是最后一个线程,调用SetEvent(hBarrierEvent)来唤醒所有等待的线程。- 离开临界区。
- 调用
WaitForSingleObject(hBarrierEvent, INFINITE)等待,如果是最后一个线程,这个调用会立即返回;否则会一直阻塞,直到最后一个线程调用SetEvent。
- 线程进入临界区,增加
-
重置:
- 如果屏障需要重复使用,在所有线程被唤醒后,需要重置事件
ResetEvent(hBarrierEvent)和计数器arrived_count = 0。
- 如果屏障需要重复使用,在所有线程被唤醒后,需要重置事件
这种方式比 pthread_barrier 更复杂,需要手动管理计数和同步,但原理是相通的。
Barrier vs. Mutex (与互斥锁的区别)
这是一个非常重要的区别,初学者很容易混淆。
| 特性 | Barrier (屏障) | Mutex (互斥锁) |
|---|---|---|
| 目的 | 同步:协调多个线程的执行进度,确保它们在某个点“集合”。 | 互斥:保护共享资源,确保同一时间只有一个线程可以访问该资源。 |
| 行为 | 等待:线程在屏障处等待,直到所有伙伴都到达。 | 锁定:线程尝试锁定一个资源,如果被锁住,就等待解锁。 |
| 粒度 | 粗粒度:作用于一个线程组,是“集体”行为。 | 细粒度:作用于一个共享变量或代码段,是“个体”行为。 |
| 比喻 | 高速公路收费站:所有车(线程)必须停在这里等,直到所有车都交了费,才能一起上路。 | 洗手间单间:只有一个人(线程)能进去,其他人必须在外面排队等着。 |
简单总结:
- 用 Mutex 来保护数据,防止数据被同时修改。
- 用 Barrier 来同步线程的执行步骤,确保所有线程都完成了某一步,才能进入下一步。
Barrier 的注意事项
- 死锁:如果参与的线程数量少于
count,那么那些到达屏障的线程将永远等待下去,造成死锁,必须确保所有线程最终都会到达屏障。 - 性能开销:
barrier会强制线程暂停和唤醒,这会带来一定的性能开销,应谨慎使用,只在确实需要同步的“全局”步骤前使用。 - 重复使用:
pthread_barrier在被所有线程通过后可以被再次使用。pthread_barrier_wait会使屏障进入“重置”状态,可以再次被count个线程等待。pthread_barrier_destroy才是永久销毁。
