C语言barrier是什么?如何使用?

99ANYc3cd6
预计阅读时长 19 分钟
位置: 首页 C语言 正文

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

C语言 barrier
(图片来源网络,侵删)

你可以把它想象成一个“起跑线”或者“集合点”:

  1. 等待:线程 A、B、C 都在执行任务,当线程 A 第一个到达 barrier 时,它会在那里停下来,等待其他伙伴。
  2. 汇聚:线程 B 和 C 也陆续到达 barrier,并停下来等待。
  3. 释放:当最后一个线程(比如线程 C)到达 barrier 时,屏障“打开”,所有等待的线程(A、B、C)被同时“唤醒”,并可以继续执行 barrier 之后的代码。

为什么需要 Barrier?

Barrier 在并行计算中非常有用,尤其是在以下场景:

  1. 分阶段计算:一个大的计算任务被分成多个阶段,每个阶段可以并行执行,但下一个阶段的开始必须等待所有线程都完成当前阶段的计算。

    • 例子:模拟一个物理系统,第一阶段,所有线程并行计算各自区域的力;第二阶段,所有线程需要基于所有区域的力来更新位置,那么在进入第二阶段前,必须有一个 barrier,确保所有力都计算完毕。
  2. 数据同步点:多个线程在并行处理数据,但在某个点,所有线程都需要访问一份共享的、完整的、一致的数据。barrier 可以确保所有线程都完成了数据的生成或预处理,然后再开始使用这份共享数据。

    C语言 barrier
    (图片来源网络,侵删)
  3. 确保线程“同频”:防止某些线程因为执行速度快而“跑得太快”,导致它们访问了尚未被其他线程准备好的数据,从而引发数据竞争或不一致。


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);

    C语言 barrier
    (图片来源网络,侵删)
    • 作用:初始化一个屏障。
    • 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,但可以用 WaitForSingleObjectSetEvent 来模拟。

  1. 初始化

    • hBarrierEvent = CreateEvent(NULL, TRUE, TRUE, NULL); // 创建一个手动重置的事件,初始状态为已触发。
    • InitializeCriticalSection(&cs);
  2. 等待函数

    • 线程进入临界区,增加 arrived_count
    • arrived_count == total_threads,说明自己是最后一个线程,调用 SetEvent(hBarrierEvent) 来唤醒所有等待的线程。
    • 离开临界区。
    • 调用 WaitForSingleObject(hBarrierEvent, INFINITE) 等待,如果是最后一个线程,这个调用会立即返回;否则会一直阻塞,直到最后一个线程调用 SetEvent
  3. 重置

    • 如果屏障需要重复使用,在所有线程被唤醒后,需要重置事件 ResetEvent(hBarrierEvent) 和计数器 arrived_count = 0

这种方式比 pthread_barrier 更复杂,需要手动管理计数和同步,但原理是相通的。


Barrier vs. Mutex (与互斥锁的区别)

这是一个非常重要的区别,初学者很容易混淆。

特性 Barrier (屏障) Mutex (互斥锁)
目的 同步:协调多个线程的执行进度,确保它们在某个点“集合”。 互斥:保护共享资源,确保同一时间只有一个线程可以访问该资源。
行为 等待:线程在屏障处等待,直到所有伙伴都到达。 锁定:线程尝试锁定一个资源,如果被锁住,就等待解锁。
粒度 粗粒度:作用于一个线程组,是“集体”行为。 细粒度:作用于一个共享变量或代码段,是“个体”行为。
比喻 高速公路收费站:所有车(线程)必须停在这里等,直到所有车都交了费,才能一起上路。 洗手间单间:只有一个人(线程)能进去,其他人必须在外面排队等着。

简单总结:

  • Mutex 来保护数据,防止数据被同时修改。
  • Barrier 来同步线程的执行步骤,确保所有线程都完成了某一步,才能进入下一步。

Barrier 的注意事项

  1. 死锁:如果参与的线程数量少于 count,那么那些到达屏障的线程将永远等待下去,造成死锁,必须确保所有线程最终都会到达屏障。
  2. 性能开销barrier 会强制线程暂停和唤醒,这会带来一定的性能开销,应谨慎使用,只在确实需要同步的“全局”步骤前使用。
  3. 重复使用pthread_barrier 在被所有线程通过后可以被再次使用。pthread_barrier_wait 会使屏障进入“重置”状态,可以再次被 count 个线程等待。pthread_barrier_destroy 才是永久销毁。
-- 展开阅读全文 --
头像
dede注册页面仿制
« 上一篇 04-12
C语言event如何实现线程同步?
下一篇 » 04-12

相关文章

取消
微信二维码
支付宝二维码

目录[+]