1. std::latch:C++20线程同步新利器
在C++20标准中,std::latch作为全新的线程同步原语闪亮登场。这个看似简单的"门闩"机制,实际上为多线程编程带来了革命性的简化。作为一名长期奋战在C++并发编程一线的开发者,我亲身体验了从mutex+condition_variable的繁琐到latch的优雅转变,今天就来深入剖析这个利器。
std::latch本质上是一个一次性使用的同步屏障,它的核心思想非常简单:初始化时设置一个计数器,线程可以通过count_down()减少这个计数器,或者通过wait()等待计数器归零。当计数器减到0时,所有等待的线程都会被自动唤醒。这种机制特别适合"主线程等待多个工作线程完成初始化"、"多个线程等待启动信号"等常见场景。
1.1 核心特性深度解析
1.1.1 一次性同步机制
std::latch最显著的特点就是它的"一次性":一旦计数器归零,这个latch就完成了它的使命,不能再重复使用。这与后面会介绍的std::barrier形成鲜明对比。这种特性使得它在以下场景中特别有用:
- 系统初始化阶段,主线程需要等待所有组件初始化完成
- 批量任务处理,等待所有工作线程完成任务报告
- 测试代码中同步多个测试线程的执行进度
cpp复制std::latch init_latch(3); // 初始化计数器为3
// 工作线程初始化完成后调用count_down()
auto worker_init = [&] {
// ...初始化工作...
init_latch.count_down();
};
// 主线程等待所有初始化完成
init_latch.wait();
1.1.2 线程安全保证
std::latch的线程安全设计非常精妙:除了析构函数外,所有成员函数的并发调用都不会导致数据竞争。这意味着:
- 多个线程可以同时调用
count_down() - 多个线程可以同时调用
wait() - 混合调用这些函数也是安全的
这种线程安全性是通过底层原子操作或内部互斥锁实现的,开发者无需额外加锁。不过这里有个重要例外:当latch对象正在被析构时,其他线程不能调用它的任何成员函数,否则会导致未定义行为。
实际经验:我曾在一个高并发服务中错误地在析构函数中直接销毁latch,而某些工作线程仍在尝试调用
count_down(),结果导致了难以追踪的崩溃。正确的做法是确保所有线程都已完成对latch的使用后再销毁它。
1.1.3 计数器类型与限制
std::latch的计数器类型是std::ptrdiff_t,这是一个有符号整数类型,通常与指针大小相同。这意味着:
- 最大计数值由
latch::max()决定,不同平台可能有不同实现 - 初始化时计数值必须非负
count_down()的参数也必须非负,且不能使计数器变为负值
cpp复制std::latch l(5); // 正确
l.count_down(3); // 正确,计数器变为2
l.count_down(3); // 错误!尝试减3但当前只有2
1.2 核心成员函数实战指南
1.2.1 count_down():非阻塞式计数
count_down()是latch的核心操作之一,它有以下几个关键特点:
- 非阻塞性:调用后立即返回,不会阻塞当前线程
- 原子性:计数器递减操作是原子的,不会与其他线程的操作产生竞争
- 唤醒机制:如果递减后计数器归零,会自动唤醒所有等待的线程
cpp复制std::latch task_latch(3);
auto worker = [&](int id) {
// 模拟工作
std::this_thread::sleep_for(100ms);
task_latch.count_down(); // 非阻塞递减
std::cout << "Worker " << id << " completed\n";
};
1.2.2 wait():阻塞式等待
wait()是另一个核心操作,它的行为特点包括:
- 阻塞性:如果计数器不为零,调用线程会被阻塞
- 自动唤醒:当计数器归零时,所有等待线程会被自动唤醒
- 即时返回:如果计数器已经为零,调用会立即返回
cpp复制std::latch start_latch(1); // 初始为1,作为"启动开关"
auto worker = [&] {
start_latch.wait(); // 等待启动信号
// ...执行工作...
};
// 主线程准备完成后"释放"所有工作线程
start_latch.count_down();
1.2.3 arrive_and_wait():组合操作
这个便捷函数相当于count_down(1)和wait()的组合,但以原子方式执行。它在"所有线程互相等待"的场景中特别有用:
cpp复制std::latch sync_latch(3); // 3个线程互相等待
auto worker = [&] {
// ...第一阶段工作...
sync_latch.arrive_and_wait(); // 递减并等待
// ...第二阶段工作(所有线程同步后继续)...
};
1.2.4 try_wait():非阻塞检查
try_wait()提供了一种非阻塞的方式来检查计数器状态:
- 非阻塞:无论计数器状态如何,都会立即返回
- 只读:不会修改计数器状态
- 返回值:true表示计数器已归零,false表示还未归零
cpp复制while(!my_latch.try_wait()) {
// 在等待时可以做一些其他工作
do_some_background_task();
}
1.3 典型使用场景与最佳实践
1.3.1 主线程等待多个工作线程
这是std::latch最经典的应用场景:
c复制
