【C++】 一文读懂 std::latch
std::latch 是 C++20 引入的新特性,本文将带你从基础到高级逐步了解它。只需 5 分钟,你就能轻松掌握 std::latch 的核心用法。
一、基础概念与核心功能
1. 什么是std::latch
?
std::latch
是C++20引入的单次使用的同步原语,用于协调多个线程的执行。它通过一个向下计数器实现同步,当计数器减至零时,所有等待的线程被唤醒。
核心特性:
- 单次使用:计数器归零后无法重置或复用。
- 线程安全:成员函数(除析构函数)可被多线程并发调用。
- 非阻塞操作:count_down()立即返回,wait()阻塞直到计数器归零。
2. 核心成员函数
函数名 | 行为描述 |
---|---|
count_down(n=1) | 非阻塞地减少计数器(默认减1),若计数器归零则唤醒所有等待线程 |
try_wait() | 非阻塞检查计数器是否为零,返回布尔值 |
wait() | 阻塞当前线程,直到计数器归零 |
arrive_and_wait() | 组合操作:count_down()后立即调用wait() |
3. 基本使用示例
#include <latch>
#include <thread>std::latch sync_point(3); // 初始计数器为3void worker() {// ... 执行任务sync_point.count_down(); // 任务完成,计数器减1
}int main() {std::thread t1(worker), t2(worker), t3(worker);sync_point.wait(); // 主线程等待所有任务完成t1.join(); t2.join(); t3.join();
}
此例中,主线程通过wait()阻塞,直到3个工作线程调用count_down()使计数器归零。
二、进阶特性与实现原理
1. 与std::atomic
的对比
传统使用原子变量实现类似功能:
std::atomic<int> counter(3);
// 线程中:counter.fetch_sub(1);
while (counter > 0); // 忙等待(低效)
std::latch
的优势:
- 避免忙等待:通过条件变量实现阻塞,减少CPU资源浪费。
- 代码简洁性:无需手动管理同步逻辑。
2. 内部实现机制
std::latch通常通过原子计数器+条件变量+互斥锁实现:
class latch {std::atomic<int> count_;std::mutex mtx_;std::condition_variable cv_;
public:explicit latch(int count) : count_(count) {}void count_down() {std::lock_guard lock(mtx_);if (--count_ == 0) cv_.notify_all();}void wait() {std::unique_lock lock(mtx_);cv_.wait(lock, [this]{ return count_ == 0; });}
};
源码贴这儿,对照着看:
_STD_BEGIN_EXPORT_STD class latch {
public:_NODISCARD static constexpr ptrdiff_t(max)() noexcept {return PTRDIFF_MAX;}constexpr explicit latch(const ptrdiff_t _Expected) noexcept /* strengthened */ : _Counter{_Expected} {_STL_VERIFY(_Expected >= 0, "Precondition: expected >= 0 (N4950 [thread.latch.class]/4)");}latch(const latch&) = delete;latch& operator=(const latch&) = delete;void count_down(const ptrdiff_t _Update = 1) noexcept /* strengthened */ {_STL_VERIFY(_Update >= 0, "Precondition: update >= 0 (N4950 [thread.latch.class]/7)");// TRANSITION, GH-1133: should be memory_order_releaseconst ptrdiff_t _Current = _Counter.fetch_sub(_Update) - _Update;if (_Current == 0) {_Counter.notify_all();} else {_STL_VERIFY(_Current >= 0, "Precondition: update <= counter (N4950 [thread.latch.class]/7)");}}_NODISCARD_TRY_WAIT bool try_wait() const noexcept {// TRANSITION, GH-1133: should be memory_order_acquirereturn _Counter.load() == 0;}void wait() const noexcept /* strengthened */ {for (;;) {// TRANSITION, GH-1133: should be memory_order_acquireconst ptrdiff_t _Current = _Counter.load();if (_Current == 0) {return;} else {_STL_VERIFY(_Current > 0, "Invariant counter >= 0, possibly caused by preconditions violation ""(N4950 [thread.latch.class]/7)");}_Counter.wait(_Current, memory_order_relaxed);}}void arrive_and_wait(const ptrdiff_t _Update = 1) noexcept /* strengthened */ {_STL_VERIFY(_Update >= 0, "Precondition: update >= 0 (N4950 [thread.latch.class]/7)");// TRANSITION, GH-1133: should be memory_order_acq_relconst ptrdiff_t _Current = _Counter.fetch_sub(_Update) - _Update;if (_Current == 0) {_Counter.notify_all();} else {_STL_VERIFY(_Current > 0, "Precondition: update <= counter (N4950 [thread.latch.class]/7)");_Counter.wait(_Current, memory_order_relaxed);wait();}}private:atomic<ptrdiff_t> _Counter;
};_STD_END
三、高级用法与场景分析
1. 组合操作arrive_and_wait()
适用于需要同时减少计数并等待的场景:
void worker(std::latch& latch) {// ... 第一阶段任务latch.arrive_and_wait(); // 减1后等待其他线程// ... 第二阶段任务(所有线程同步后继续)
}
此操作简化代码,避免手动分开调用count_down()
和wait()
。
2. 资源初始化同步
确保所有资源初始化完成后主线程继续:
#include <latch>
#include <thread>
#include <vector>
#include <iostream>
using namespace std;std::latch sync_point(3); // 初始计数器为3struct Resource {void load_data() {std::cout << __FUNCTION__ << " loading data..." << std::this_thread::get_id() << std::endl;}
};std::latch init_latch(5);
std::vector<Resource> resources(5);void init_resource(int id) {resources[id].load_data(); // 初始化资源init_latch.count_down();
}int main() {for (int i = 0; i < 5; ++i)std::thread(init_resource, i).detach();init_latch.wait(); // 等待所有资源初始化完成// 使用资源...
}
3. 与std::barrier
的对比
特性 | std::latch | std::barrier |
---|---|---|
复用性 | 单次使用 | 可重复使用 |
阶段控制 | 无 | 支持多阶段同步 |
适用场景 | 一次性任务(如初始化) | 分阶段并行算法(如迭代计算) |
四、常见问题与调试技巧
1. 计数器未归零导致死锁
问题:若count_down()
调用次数不足,wait()将永久阻塞。
解决:确保每个线程严格调用一次count_down()
。
2. 错误复用
问题:尝试复用已归零的latch
会导致未定义行为。
解决:需重新创建新对象,或改用std::barrier
。
3. 生命周期管理
陷阱:若latch
对象在wait()
调用前被销毁,导致悬垂引用。
解决:确保latch
的生命周期覆盖所有线程操作。
五、实际应用案例
1. 并行任务分阶段执行
void phased_processing() {std::latch phase1(4), phase2(4);auto worker = [&] {// 阶段1phase1.arrive_and_wait();// 阶段2phase2.arrive_and_wait();};std::jthread t1(worker), t2(worker), t3(worker), t4(worker);
}
此模式需配合多个latch
对象实现多阶段同步(更推荐使用std::barrier
)
2. 动态线程池任务协调
class Task
{
public:void excute(){}
};void process_batch(std::vector<Task>& tasks) {std::latch completion_latch(tasks.size());for (auto& task : tasks) {thread_pool.submit([&] {task.execute();completion_latch.count_down();});}completion_latch.wait(); // 等待所有任务完成
}
六、总结
std::latch
作为C++20引入的线程同步工具,为一次性多线程协作提供了简洁高效的解决方案。它通过封装同步逻辑,不仅简化了代码实现,还显著提升了可读性。与std::latch
不同,std::barrier
更适合需要重复同步的场景。在实际应用中,开发者应特别关注其生命周期管理,并确保计数器调用的准确性,以规避潜在问题。