最近了解了一下C++20新加入的std::latch
(门闩)和std::barrier
(屏障),它们是用来在多线程程序中让线程之间进行协调的机制,具体可以在网上搜索或者问AI,这里就不展开了。
本文主要是想在一个函数里把这两个机制都用上,以使读者有更直观的感受。本文还用到了std::barrier
的完成回调函数机制,它会在每一次所有线程都到达屏障的时候执行。
程序的主体部分是ChatGPT(o3-mini)写的,然后改了一下bug。一开始ChatGPT写出来的程序对于worker函数签名里的barrier类型声明有误,漏了barrier的模板参数,问了kimi半天没解决问题,问了DeepSeek一下子就找到问题所在了,可惜DeepSeek太卡了,要重试个十几二十几次才能回答。
一些细节的讨论在程序的注释里。
话不多说,上代码!
代码
编译的时候需要设置语言版本为C++23(
-std=c++23
)。
本程序在Windows上使用GCC 14.2.0(MinGW-W64)测试通过。也在Compiler Explorer上使用clang (trunk)和gcc (trunk)测试通过。
程序的每一条std::println
旁边都有一条std::osyncstream(std::cout)
(要使用的话要#include <syncstream>
),它们的效果是一样的。但是如果直接使用std::cout
的话,则会出现多线程输出内容交织的情况。
#include <iostream>
#include <print>
#include <vector>
#include <thread>
#include <latch>
#include <barrier>
#include <chrono>
// #include <syncstream>
#include <functional>
// 为了使用 1s、1000ms 这样的用户自定义字面量,这样更直观
using namespace std::chrono_literals;
// 完成回调函数可以定义成 lambda 函数(第一行),也可以包装在 std::function 里(第二行)
// 如果是前者,那么下面 worker 的签名也要用第一行的,后者的话下面 worker 的签名第一行和第二行的都可以
// 用 std::function 的好处在于,completion_function 不一定要定义在 worker 前面
// 如果用 lambda 函数,那么 worker 的签名要用到 decltype(completion_function) ,
// 就需要 completion_function 定义在 worker 前面
// auto completion_function = []
auto completion_function = std::function([]
{
// std::osyncstream(std::cout) << "All threads reached the barrier. Proceeding to next phase.\n";
std::println("All threads reached the barrier. Proceeding to next phase.");
});
// std::barrier 的模板参数是完成回调函数的类型,如果要用到完成回调函数,那么不可省略
// void worker(std::latch &latch, std::barrier<decltype(completion_function)> &barrier, int threadID)
void worker(std::latch &latch, std::barrier<std::function<void()>> &barrier, int threadID)
{
// 模拟初始工作(不同线程耗时不同)
// std::osyncstream(std::cout) << "Thread " << threadID << " is performing initial work.\n";
std::println("Thread {} is performing initial work.", threadID);
std::this_thread::sleep_for(1000ms * threadID);
// std::osyncstream(std::cout) << "Thread " << threadID << " finished initial work, counting down latch.\n";
std::println("Thread {} finished initial work, counting down latch.", threadID);
// latch 部分:每个线程调用 count_down() 后等待
latch.count_down();
latch.wait();
// std::osyncstream(std::cout) << "Thread " << threadID << " passed latch synchronization.\n";
std::println("Thread {} passed latch synchronization.", threadID);
// barrier 部分:进行3个阶段的同步
for (int phase = 1; phase <= 3; phase++) {
// std::osyncstream(std::cout) << "Thread " << threadID << " is working on barrier phase " << phase << ".\n";
std::println("Thread {} is working on barrier phase {}.", threadID, phase);
std::this_thread::sleep_for(1000ms);
// std::osyncstream(std::cout) << "Thread " << threadID << " reached barrier phase " << phase << ".\n";
std::println("Thread {} reached barrier phase {}.", threadID, phase);
barrier.arrive_and_wait();
}
}
int main()
{
const int num_threads = 3;
// 创建 latch 对象,计数器初值为线程数量
std::latch latch(num_threads);
// 创建 barrier 对象,参与线程数为 num_threads,设置回调函数在每个阶段结束时执行
std::barrier barrier(num_threads, completion_function);
std::vector<std::jthread> threads;
// 创建 num_threads 个线程,全部执行 worker 函数
for (int i = 0; i < num_threads; i++) {
threads.emplace_back(worker, std::ref(latch), std::ref(barrier), i + 1);
}
// std::jthread 会在析构的时候自动 join(),不需要显式地 join()
// for (auto &t : threads) {
// t.join();
// }
return 0;
}
样例输出
供参考,其中不同线程之间输出行的顺序是没有保证的,每一次运行都是不一样的。
Thread 1 is performing initial work.
Thread 3 is performing initial work.
Thread 2 is performing initial work.
Thread 1 finished initial work, counting down latch.
Thread 2 finished initial work, counting down latch.
Thread 3 finished initial work, counting down latch.
Thread 3 passed latch synchronization.
Thread 1 passed latch synchronization.
Thread 1 is working on barrier phase 1.
Thread 2 passed latch synchronization.
Thread 2 is working on barrier phase 1.
Thread 3 is working on barrier phase 1.
Thread 1 reached barrier phase 1.
Thread 2 reached barrier phase 1.
Thread 3 reached barrier phase 1.
All threads reached the barrier. Proceeding to next phase.
Thread 2 is working on barrier phase 2.
Thread 3 is working on barrier phase 2.
Thread 1 is working on barrier phase 2.
Thread 2 reached barrier phase 2.
Thread 3 reached barrier phase 2.
Thread 1 reached barrier phase 2.
All threads reached the barrier. Proceeding to next phase.
Thread 1 is working on barrier phase 3.
Thread 3 is working on barrier phase 3.
Thread 2 is working on barrier phase 3.
Thread 1 reached barrier phase 3.
Thread 3 reached barrier phase 3.
Thread 2 reached barrier phase 3.
All threads reached the barrier. Proceeding to next phase.