文章目录
- std::latch
-
- 简单介绍
-
- [一、std::latch 深度解析](#一、std::latch 深度解析)
-
- [1. 核心特性](#1. 核心特性)
- [2. 核心成员函数](#2. 核心成员函数)
- [3. 典型使用场景](#3. 典型使用场景)
- [二、std::latch 与 mutex/condition_variable 的对比](#二、std::latch 与 mutex/condition_variable 的对比)
-
- [1. 核心联系](#1. 核心联系)
- [2. 核心区别](#2. 核心区别)
- [3. 代码对比:实现"主线程等3个子线程完成"](#3. 代码对比:实现“主线程等3个子线程完成”)
-
- [方式1:用 std::latch(简洁)](#方式1:用 std::latch(简洁))
- [方式2:用 mutex + condition_variable(繁琐)](#方式2:用 mutex + condition_variable(繁琐))
- 线程安全和非阻塞
-
- 一、线程安全------"除析构函数外,所有成员函数的并发调用不会导致数据竞争"
-
- [1. 核心概念拆解](#1. 核心概念拆解)
- [2. 通俗解释](#2. 通俗解释)
- [3. 反面例子(错误用法,析构时仍有线程使用)](#3. 反面例子(错误用法,析构时仍有线程使用))
- [4. 正确用法(析构前确保无线程使用)](#4. 正确用法(析构前确保无线程使用))
- [二、理解"非阻塞式"函数:count_down() 和 try_wait()](#二、理解“非阻塞式”函数:count_down() 和 try_wait())
-
- [1. 核心概念:阻塞 vs 非阻塞](#1. 核心概念:阻塞 vs 非阻塞)
- [2. std::latch::count_down(n=1) 详解](#2. std::latch::count_down(n=1) 详解)
- [3. std::latch::try_wait() 详解](#3. std::latch::try_wait() 详解)
- [4. 代码示例:直观理解非阻塞特性](#4. 代码示例:直观理解非阻塞特性)
- [5. 对比:count_down() + try_wait() vs wait()](#5. 对比:count_down() + try_wait() vs wait())
- 总结
- 总结
- std::barrier
-
-
- [一、std::barrier 深度解析](#一、std::barrier 深度解析)
-
- [1. 核心特性](#1. 核心特性)
- [2. 核心成员函数](#2. 核心成员函数)
- [3. 典型使用场景](#3. 典型使用场景)
- [二、std::barrier 丰富代码示例](#二、std::barrier 丰富代码示例)
-
- [示例 1:基础用法(多轮同步)](#示例 1:基础用法(多轮同步))
- [示例 2:带完成回调的 barrier](#示例 2:带完成回调的 barrier)
- [示例 3:arrive_and_drop() 用法(动态减少参与数)](#示例 3:arrive_and_drop() 用法(动态减少参与数))
- [三、barrier vs latch 核心对比(补充完整)](#三、barrier vs latch 核心对比(补充完整))
- [四、barrier vs mutex+condition_variable 对比](#四、barrier vs mutex+condition_variable 对比)
-
- [用 mutex+cv 实现"2轮3线程同步"(等价于示例1的 barrier 逻辑)](#用 mutex+cv 实现“2轮3线程同步”(等价于示例1的 barrier 逻辑))
- 总结
-
std::latch
C++20 中引入的 std::latch,包括它的核心用法、工作原理,以及它和传统的 std::mutex、std::condition_variable 在功能、使用场景上的区别与联系。
简单介绍
一、std::latch 深度解析
std::latch(门闩)是 C++20 并发库中新增的一次性同步屏障,核心作用是让一个或多个线程等待,直到预设数量的线程完成各自的操作(计数器减至 0)。
1. 核心特性
- 一次性 :计数器只能递减,无法重置或增加,一旦减到 0,所有等待的线程被唤醒,后续调用
wait()不会阻塞(相当于"永久打开")。 - 计数器类型 :底层是
std::ptrdiff_t(有符号整数),最大值由latch::max()决定(不同编译器实现不同)。 - 线程安全:除析构函数外,所有成员函数的并发调用不会导致数据竞争。
2. 核心成员函数
| 函数 | 功能 |
|---|---|
| 构造函数 | std::latch l(n);:初始化计数器为 n(n ≥ 0) |
count_down(n=1) |
非阻塞式将计数器减 n,若减到 0,唤醒所有等待的线程 |
try_wait() |
非阻塞式检查计数器是否为 0,返回 true/false(不阻塞) |
wait() |
阻塞当前线程,直到计数器为 0(若已为 0,直接返回) |
arrive_and_wait() |
等价于 count_down(1); wait();:先减 1,再等待计数器为 0 |
3. 典型使用场景
- 主线程等待多个子线程完成初始化:比如主线程启动 5 个工作线程,需等所有线程初始化完毕后,主线程才继续执行。
- 多个线程等待某个"总开关" :比如所有工作线程先等待主线程发出"开始"信号(计数器初始为 1,主线程调用
count_down()后,所有线程同时启动)。
二、std::latch 与 mutex/condition_variable 的对比
std::mutex(互斥锁)和 std::condition_variable(条件变量)是 C++11 就有的同步原语,而 std::latch 是更高层的封装,三者的核心区别和联系如下:
1. 核心联系
- 都是用于线程同步的工具,解决多线程间的执行顺序协调问题。
std::latch的底层实现通常依赖mutex + condition_variable(编译器层面封装),相当于"专用同步工具",而mutex + condition_variable是"通用同步工具"。
2. 核心区别
| 维度 | std::latch | mutex + condition_variable |
|---|---|---|
| 抽象层级 | 高层封装(专用同步屏障) | 底层原语(通用同步) |
| 使用方式 | 基于计数器,只需关注"计数减到 0" | 需手动加锁、解锁,手动判断条件 |
| 复用性 | 一次性(计数器到 0 后失效) | 可重复使用(条件可多次满足/不满足) |
| 代码复杂度 | 极简(几行代码完成同步) | 较复杂(需处理锁、条件判断、虚假唤醒) |
| 适用场景 | 固定数量线程的一次性同步 | 任意复杂的线程同步(如生产者消费者) |
| 性能 | 更优(编译器优化的专用逻辑) | 通用逻辑,需手动处理细节,易有冗余 |
3. 代码对比:实现"主线程等3个子线程完成"
方式1:用 std::latch(简洁)
cpp
#include <latch>
#include <thread>
#include <iostream>
int main() {
std::latch latch(3); // 初始化计数器为3
auto work = [&](const std::string& name) {
std::cout << name << " 完成工作\n";
latch.count_down(); // 计数器减1
};
std::thread t1(work, "线程1");
std::thread t2(work, "线程2");
std::thread t3(work, "线程3");
latch.wait(); // 主线程阻塞,直到计数器为0
std::cout << "所有线程完成工作,主线程继续\n";
t1.join(); t2.join(); t3.join();
return 0;
}
方式2:用 mutex + condition_variable(繁琐)
cpp
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>
int main() {
std::mutex mtx;
std::condition_variable cv;
int counter = 3; // 手动维护计数器
auto work = [&](const std::string& name) {
std::cout << name << " 完成工作\n";
std::lock_guard<std::mutex> lock(mtx);
counter--;
if (counter == 0) {
cv.notify_all(); // 计数器为0,唤醒主线程
}
};
std::thread t1(work, "线程1");
std::thread t2(work, "线程2");
std::thread t3(work, "线程3");
// 主线程等待
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [&]() { return counter == 0; }); // 处理虚假唤醒
std::cout << "所有线程完成工作,主线程继续\n";
t1.join(); t2.join(); t3.join();
return 0;
}
可以看到:latch 省去了手动加锁、解锁、处理虚假唤醒的繁琐,代码更简洁、不易出错。
你想深入理解 std::latch 两个核心特性:一是"除析构外所有成员函数并发调用无数据竞争"的线程安全含义,二是 count_down()、try_wait() 这两个"非阻塞式"函数的具体行为和底层逻辑。
线程安全和非阻塞
一、线程安全------"除析构函数外,所有成员函数的并发调用不会导致数据竞争"
1. 核心概念拆解
- 数据竞争(Data Race):多线程同时访问同一个非原子变量,且至少有一个线程是"写操作",且没有同步机制------这是 C++ 未定义行为的核心来源(可能导致程序崩溃、结果错乱)。
- 并发调用成员函数 :多个线程同时调用同一个
std::latch对象的成员函数(比如线程1调用count_down(),线程2调用wait(),线程3调用try_wait())。 - 析构函数例外 :如果一个线程在析构
std::latch对象时,还有其他线程在调用它的成员函数(如wait()/count_down()),会导致未定义行为(崩溃、死锁等)------这是你使用时必须手动保证的:析构前确保所有线程都不再使用该latch。
2. 通俗解释
std::latch 的设计者已经帮你处理了内部的同步问题:
- 你无需手动加锁(比如
std::mutex),就能让多个线程安全地调用同一个latch的count_down()、wait()、try_wait()等函数; - 这些函数的并发调用不会触发"数据竞争",底层通过原子操作(
std::atomic)或内部锁保证了计数器操作的原子性和可见性; - 唯一需要你负责的是:析构
latch前,必须确保所有线程都已经完成对它的所有操作(比如所有wait()的线程都已被唤醒,所有count_down()都已执行完毕)。
3. 反面例子(错误用法,析构时仍有线程使用)
cpp
#include <latch>
#include <thread>
int main() {
std::latch* l = new std::latch(1);
// 线程1:一直等待latch(永远不会被唤醒)
std::thread t1([&]() {
l->wait(); // 阻塞在这里
});
// 主线程:直接析构latch(此时线程1还在调用wait())
delete l; // 未定义行为!数据竞争+析构时使用,可能崩溃
t1.join();
return 0;
}
4. 正确用法(析构前确保无线程使用)
cpp
#include <latch>
#include <thread>
int main() {
std::latch l(1);
std::thread t1([&]() {
l.wait(); // 等待
std::cout << "线程1被唤醒\n";
});
l.count_down(); // 计数器减到0,唤醒线程1
t1.join(); // 确保线程1已完成对l的所有操作
// 此时析构l(自动析构)是安全的
return 0;
}
二、理解"非阻塞式"函数:count_down() 和 try_wait()
1. 核心概念:阻塞 vs 非阻塞
- 阻塞函数 :调用后,当前线程会暂停执行(进入等待状态),直到某个条件满足(比如
wait()会阻塞到计数器为0); - 非阻塞函数:调用后,当前线程不会暂停,无论条件是否满足,函数都会立即返回,只做"一次性操作/检查"。
2. std::latch::count_down(n=1) 详解
- 核心行为 :
- 非阻塞:调用后,线程不会停,立即执行后续代码;
- 原子操作:将内部计数器原子性地减去
n(默认减1),不会和其他线程的count_down()/wait()等操作产生数据竞争; - 唤醒逻辑:如果减完后计数器等于0,会自动唤醒所有正在
wait()/arrive_and_wait()上阻塞的线程(这个唤醒是底层自动做的,你无需手动处理)。
- 通俗例子 :
你可以把latch想象成"有N把锁的门",count_down()就是"一次性取下n把锁",取锁的过程不会耽误你做其他事(非阻塞);如果取完后锁的数量为0,门就会自动打开,所有等在门口的人(线程)都能进去。
3. std::latch::try_wait() 详解
- 核心行为 :
- 非阻塞:调用后立即返回,线程不会暂停;
- 仅检查:原子性地检查内部计数器是否等于0;
- 返回值:
true:计数器已为0(门已开);false:计数器还没到0(门还关着)。
- 关键区别 :
try_wait()只是"看一眼"状态,不会修改计数器,也不会阻塞------和wait()形成鲜明对比:wait():如果门没开,就站在门口等(阻塞);如果门开了,直接过;try_wait():如果门没开,扭头就走(返回false);如果门开了,也只是告诉你"门开了"(返回true),但不会"过门槛"(不影响计数器)。
4. 代码示例:直观理解非阻塞特性
cpp
#include <latch>
#include <thread>
#include <iostream>
#include <chrono>
int main() {
std::latch l(2); // 计数器初始为2
// 线程1:调用count_down(非阻塞)
std::thread t1([&]() {
std::cout << "线程1:调用count_down前\n";
l.count_down(1); // 计数器变为1,非阻塞,立即执行下一行
std::cout << "线程1:调用count_down后(未阻塞)\n";
// 这里可以继续做其他事,不会等
std::this_thread::sleep_for(std::chrono::seconds(1));
l.count_down(1); // 计数器变为0,唤醒等待的线程
});
// 主线程:循环调用try_wait(非阻塞检查)
int check_times = 0;
while (true) {
check_times++;
bool is_zero = l.try_wait(); // 非阻塞,立即返回
std::cout << "主线程:第" << check_times << "次检查,计数器是否为0:" << std::boolalpha << is_zero << "\n";
if (is_zero) {
break; // 计数器为0,退出循环
}
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟其他工作
}
t1.join();
std::cout << "最终:计数器为0,所有操作完成\n";
return 0;
}
输出结果:
线程1:调用count_down前
线程1:调用count_down后(未阻塞)
主线程:第1次检查,计数器是否为0:false
主线程:第2次检查,计数器是否为0:false
主线程:第3次检查,计数器是否为0:true
最终:计数器为0,所有操作完成
输出分析:
count_down()是非阻塞的:线程1调用后立即打印"调用后"的日志,没有等待;try_wait()是非阻塞的:主线程每次调用都立即得到结果,循环中可以穿插其他操作(比如sleep_for模拟工作);- 当线程1第二次调用
count_down()使计数器为0后,主线程的try_wait()才返回true。
5. 对比:count_down() + try_wait() vs wait()
如果你想实现"等待计数器为0,但期间可以做其他事",可以用 try_wait() 循环;而 wait() 是"死等",期间不做任何事:
cpp
// 方式1:wait()(阻塞,期间不做任何事)
l.wait(); // 线程暂停,直到计数器为0
std::cout << "计数器为0\n";
// 方式2:try_wait() 循环(非阻塞检查,期间可做其他事)
while (!l.try_wait()) {
std::cout << "计数器还没到0,先做点其他事...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "计数器为0\n";
总结
- 线程安全核心 :
std::latch内部已保证成员函数(除析构)并发调用无数据竞争,无需手动加锁;但必须保证析构前所有线程都已完成对它的操作。 - count_down() 核心:非阻塞式原子减计数器,减到0时自动唤醒所有等待线程,调用后线程立即继续执行。
- try_wait() 核心:非阻塞式检查计数器是否为0,仅返回状态不修改计数器,适合"轮询检查"场景(不想阻塞线程时用)。
简单记:
count_down():"做事(减计数)不耽误"(非阻塞修改);try_wait():"看一眼不做事"(非阻塞检查);wait():"死等直到事成"(阻塞等待);- 线程安全:"除了析构,随便调"(底层已处理同步)。
总结
- std::latch 核心:C++20 新增的一次性同步屏障,基于递减计数器实现,适合固定数量线程的一次性同步,代码简洁、性能优。
- 与 mutex+cv 的区别:latch 是高层封装的"专用工具",一次性、无需手动处理锁和条件;mutex+cv 是底层"通用工具",可复用、适合复杂同步场景,但代码繁琐。
- 联系:latch 底层通常由 mutex+cv 实现,是对"等待计数器为0"这一常见同步场景的封装,简化了开发。
简单来说:如果需求是"等 N 个线程做完某件事,之后不再重复这个等待逻辑",优先用 std::latch;如果是更复杂的同步(如生产者消费者、动态条件判断),再用 mutex + condition_variable。
std::barrier
你希望我在上一轮对 std::latch 讲解的基础上,大幅扩展 std::barrier 的内容(包括核心特性、使用场景、成员函数),并提供丰富且易理解的代码示例,同时对比它和 std::latch 的差异。
一、std::barrier 深度解析
std::barrier(屏障)是 C++20 并发库中与 std::latch 并列的同步原语,核心定位是可复用的线程同步屏障 ------它允许固定数量的线程互相等待,直到所有线程都到达屏障点,然后一起继续执行;执行完一轮后,屏障可自动重置,支持多轮同步(这是和 latch 最核心的区别)。
1. 核心特性
- 可复用性:每轮所有线程到达屏障后,计数器自动重置为初始值,可重复使用。
- 固定参与数 :初始化时指定"参与线程数"(
std::ptrdiff_t类型),只有当该数量的线程都调用arrive_and_wait()后,屏障才会放行。 - 完成阶段(Completion Phase):可选的回调函数,在所有线程到达屏障、且放行前执行(由最后一个到达的线程执行)。
- 线程安全:除析构函数外,成员函数并发调用无数据竞争。
- 不可动态修改参与数 :初始化后,参与线程数无法更改(若需动态调整,可使用 C++20 的
std::flex_barrier,但部分编译器暂未完全支持)。
2. 核心成员函数
| 函数 | 功能 |
|---|---|
| 构造函数 | std::barrier b(n, [](auto&&){/* 完成回调 */});:初始化参与数 n,可选完成回调 |
arrive_and_wait() |
当前线程到达屏障,阻塞直到所有 n 个线程都到达;放行后屏障自动重置 |
arrive_and_drop() |
当前线程到达屏障,并"退出参与"(后续轮次的参与数减 1),不阻塞 |
| (析构函数) | 销毁屏障(需确保无线程阻塞在屏障上,否则行为未定义) |
注意:
std::barrier没有count_down()/wait()这类拆分的函数,核心只有arrive_and_wait()(到达并等待)和arrive_and_drop()(到达并退出),这是因为它的核心逻辑是"所有参与线程必须都到达才放行"。
3. 典型使用场景
- 多轮并行计算:比如图像处理中,分 4 个线程处理 4 块图像,每轮处理完后同步结果,再开始下一轮处理。
- 流水线同步:多个线程分阶段执行任务,每阶段结束后所有线程同步,确保数据一致性后进入下一阶段。
- 批量任务分轮执行:比如爬虫分多轮抓取数据,每轮所有线程完成抓取后汇总,再开始下一轮。
二、std::barrier 丰富代码示例
示例 1:基础用法(多轮同步)
演示 barrier 的可复用性,3 个线程执行 2 轮任务,每轮都等待所有线程完成后再继续:
cpp
#include <barrier>
#include <thread>
#include <iostream>
#include <vector>
int main() {
// 初始化屏障:参与线程数=3,无完成回调
std::barrier sync_barrier(3);
// 工作线程函数:执行2轮任务
auto worker = [&](int id) {
for (int round = 1; round <= 2; ++round) {
// 执行本轮任务
std::cout << "线程" << id << " 执行第" << round << "轮任务\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟任务耗时
// 到达屏障,等待其他线程
std::cout << "线程" << id << " 到达第" << round << "轮屏障\n";
sync_barrier.arrive_and_wait();
// 所有线程到达后,继续执行
std::cout << "线程" << id << " 完成第" << round << "轮同步,继续\n";
}
};
// 创建3个工作线程
std::vector<std::thread> threads;
for (int i = 1; i <= 3; ++i) {
threads.emplace_back(worker, i);
}
// 等待所有线程结束
for (auto& t : threads) {
t.join();
}
return 0;
}
输出(顺序可能因线程调度略有不同,但每轮都会同步):
线程1 执行第1轮任务
线程2 执行第1轮任务
线程3 执行第1轮任务
线程1 到达第1轮屏障
线程2 到达第1轮屏障
线程3 到达第1轮屏障
线程3 完成第1轮同步,继续
线程1 完成第1轮同步,继续
线程2 完成第1轮同步,继续
线程1 执行第2轮任务
线程2 执行第2轮任务
线程3 执行第2轮任务
线程1 到达第2轮屏障
线程2 到达第2轮屏障
线程3 到达第2轮屏障
线程3 完成第2轮同步,继续
线程1 完成第2轮同步,继续
线程2 完成第2轮同步,继续
示例 2:带完成回调的 barrier
演示"完成阶段回调"的作用:最后一个到达屏障的线程执行回调(比如汇总本轮结果):
cpp
#include <barrier>
#include <thread>
#include <iostream>
#include <vector>
#include <atomic>
// 全局原子变量,模拟每轮任务的结果汇总
std::atomic<int> total_result{0};
int main() {
// 初始化屏障:参与数=3,完成回调(汇总结果并重置)
std::barrier sync_barrier(
3,
[]() {
std::cout << "\n【完成回调】本轮所有线程完成,总结果:" << total_result << "\n";
total_result = 0; // 重置结果,为下一轮做准备
}
);
auto worker = [&](int id) {
for (int round = 1; round <= 2; ++round) {
// 执行本轮任务:每个线程贡献 id*round 的值
int contribution = id * round;
total_result += contribution;
std::cout << "线程" << id << " 第" << round << "轮贡献:" << contribution << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 到达屏障
sync_barrier.arrive_and_wait();
// 回调执行后,继续
std::cout << "线程" << id << " 第" << round << "轮同步完成\n";
}
};
std::vector<std::thread> threads;
for (int i = 1; i <= 3; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
输出:
线程1 第1轮贡献:1
线程2 第1轮贡献:2
线程3 第1轮贡献:3
【完成回调】本轮所有线程完成,总结果:6
线程3 第1轮同步完成
线程1 第1轮同步完成
线程2 第1轮同步完成
线程1 第2轮贡献:2
线程2 第2轮贡献:4
线程3 第2轮贡献:6
【完成回调】本轮所有线程完成,总结果:12
线程3 第2轮同步完成
线程1 第2轮同步完成
线程2 第2轮同步完成
示例 3:arrive_and_drop() 用法(动态减少参与数)
演示线程"退出参与":某线程完成最后一轮后,调用 arrive_and_drop() 退出,后续轮次的参与数减少:
cpp
#include <barrier>
#include <thread>
#include <iostream>
#include <vector>
int main() {
// 初始参与数=3
std::barrier sync_barrier(3);
auto worker = [&](int id) {
for (int round = 1; round <= 3; ++round) {
std::cout << "线程" << id << " 执行第" << round << "轮任务\n";
if (id == 1 && round == 2) {
// 线程1在第2轮后退出参与
std::cout << "线程" << id << " 退出后续轮次\n";
sync_barrier.arrive_and_drop(); // 到达并退出,参与数变为2
break; // 线程1结束
}
// 到达屏障(线程1仅执行前2轮,后2轮不参与)
sync_barrier.arrive_and_wait();
std::cout << "线程" << id << " 完成第" << round << "轮同步\n";
}
};
std::vector<std::thread> threads;
for (int i = 1; i <= 3; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
输出:
线程1 执行第1轮任务
线程2 执行第1轮任务
线程3 执行第1轮任务
线程1 完成第1轮同步
线程2 完成第1轮同步
线程3 完成第1轮同步
线程1 执行第2轮任务
线程2 执行第2轮任务
线程3 执行第2轮任务
线程1 退出后续轮次
线程2 完成第2轮同步
线程3 完成第2轮同步
线程2 执行第3轮任务
线程3 执行第3轮任务
线程2 完成第3轮同步
线程3 完成第3轮同步
三、barrier vs latch 核心对比(补充完整)
为了更清晰区分两者,整理成表格:
| 特性 | std::barrier | std::latch |
|---|---|---|
| 复用性 | 可复用(自动重置计数器) | 一次性(计数器到0后失效) |
| 核心逻辑 | 所有 N 个线程到达后一起放行 | 等待计数器减到0(可单线程等多线程/多线程等单线程) |
| 参与数 | 初始化固定,仅可通过 arrive_and_drop() 减少 | 初始化固定,仅可递减 |
| 核心函数 | arrive_and_wait()、arrive_and_drop() | count_down()、wait()、arrive_and_wait() |
| 完成回调 | 支持(最后一个到达的线程执行) | 不支持 |
| 适用场景 | 多轮同步(如多轮计算、流水线) | 单次同步(如初始化、一次性任务汇总) |
四、barrier vs mutex+condition_variable 对比
barrier 同样是对 mutex+cv 的高层封装,对比示例:
用 mutex+cv 实现"2轮3线程同步"(等价于示例1的 barrier 逻辑)
cpp
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>
#include <vector>
#include <chrono>
int main() {
std::mutex mtx;
std::condition_variable cv;
int arrived = 0; // 已到达的线程数
const int total_workers = 3;
auto worker = [&](int id) {
for (int round = 1; round <= 2; ++round) {
// 执行本轮任务(无锁,线程安全)
std::cout << "线程" << id << " 执行第" << round << "轮任务\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 到达屏障:所有逻辑在同一个锁范围内,保证原子性
std::unique_lock<std::mutex> lock(mtx);
arrived++; // 原子更新计数器
std::cout << "线程" << id << " 到达第" << round << "轮屏障\n";
// 等待所有线程到达:wait返回时仍持有锁
cv.wait(lock, [&]() { return arrived == 0; });
// 注意:这里条件改为arrived==0(重置后的值),而非原有的arrived==total_workers
// 执行到这里,说明已被唤醒,本轮同步完成
std::cout << "线程" << id << " 完成第" << round << "轮同步,继续\n";
// 解锁:退出作用域后unique_lock自动解锁,无需手动unlock
}
};
// 单独的"屏障控制线程":负责等待所有线程到达,然后重置+唤醒
// (也可以在worker中让最后一个到达的线程执行此逻辑,这里更清晰)
std::thread barrier_controller([&]() {
for (int round = 1; round <= 2; ++round) {
std::unique_lock<std::mutex> lock(mtx);
// 等待所有线程到达(arrived == total_workers)
cv.wait(lock, [&]() { return arrived == total_workers; });
std::cout << "\n【第" << round << "轮】所有线程到达,放行\n";
// 重置计数器(仅控制器线程执行,避免竞争)
arrived = 0;
// 唤醒所有等待的worker线程
cv.notify_all();
// 解锁:退出作用域自动解锁
}
});
// 创建3个工作线程
std::vector<std::thread> threads;
for (int i = 1; i <= 3; ++i) {
threads.emplace_back(worker, i);
}
// 等待所有线程结束
barrier_controller.join();
for (auto& t : threads) {
t.join();
}
return 0;
}
可以看到:用 mutex+cv 实现等价逻辑需要手动维护计数器、处理锁、重置状态、避免虚假唤醒,代码繁琐且易出错;而 std::barrier 只需几行代码即可完成,且编译器已优化了底层逻辑,性能更优。
仅供参考
cpp
#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>
#include <vector>
#include <chrono>
int main() {
std::mutex mtx;
std::condition_variable cv;
int arrived = 0; // 已到达的线程数
const int total_workers = 3;
auto worker = [&](int id) {
for (int round = 1; round <= 2; ++round) {
// 执行本轮任务
std::cout << "线程" << id << " 执行第" << round << "轮任务\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock(mtx);
arrived++;
std::cout << "线程" << id << " 到达第" << round << "轮屏障\n";
// 情况1:不是最后一个到达的线程 → 等待唤醒
if (arrived < total_workers) {
// 等待条件:arrived被重置为0(表示本轮已放行)
cv.wait(lock, [&]() { return arrived == 0; });
}
// 情况2:最后一个到达的线程 → 重置计数器+唤醒所有线程
else {
std::cout << "\n【第" << round << "轮】所有线程到达,放行\n";
arrived = 0; // 重置
cv.notify_all(); // 唤醒其他线程
}
std::cout << "线程" << id << " 完成第" << round << "轮同步,继续\n";
}
};
std::vector<std::thread> threads;
for (int i = 1; i <= 3; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
总结
- std::barrier 核心:C++20 引入的可复用同步屏障,固定参与线程数,所有线程到达后放行并自动重置,支持完成回调,适合多轮同步场景。
- 核心函数 :
arrive_and_wait()(到达并等待)是核心,arrive_and_drop()可动态减少参与数,构造函数可指定完成回调。 - 与 latch 的区别:barrier 可复用、侧重"所有线程互相等待";latch 一次性、侧重"等待计数器到0"。
- 与 mutex+cv 的对比:barrier 是高层封装,代码简洁、无需手动处理锁和条件,性能更优;mutex+cv 是底层原语,适合复杂自定义同步场景。
简单来说:如果需要多轮 的线程同步,优先用 std::barrier;如果只需单次 同步,用 std::latch;如果同步逻辑高度自定义,再用 mutex + condition_variable。