哈喽,各位好!
今天咱们来聊聊 C++20 引入的 std::atomic
的新技能:wait
、notify_one
和 notify_all
。这哥仨儿的加入,让原子变量在线程同步方面更加游刃有余,简直是原子操作界的“完全体”。
一、 背景故事:原子变量的自我修养
在并发编程的世界里,共享数据是最容易引发混乱的根源。多个线程同时访问和修改同一块内存,就可能导致数据竞争,程序崩溃,或者出现一些神鬼莫测的bug。为了解决这个问题,C++ 提供了 std::atomic
,它能保证对原子变量的操作是原子性的,也就是不可分割的。
但是,仅仅保证原子性还不够。有时候,我们需要线程之间能够协调工作,比如一个线程需要等待某个条件成立才能继续执行,或者一个线程需要通知其他线程某个事件已经发生。在 C++20 之前,我们通常需要借助互斥锁、条件变量等更重量级的工具才能实现这些功能。
而现在,有了 wait
、notify_one
和 notify_all
,原子变量也能胜任这些任务了。
二、 三剑客登场:wait
, notify_one
, notify_all
这三个函数,就像是原子变量的“睡眠”、“叫醒”和“集体起床”功能。
wait(value)
: 让线程休眠,直到原子变量的值发生改变,并且不等于value
。 这就像线程说:“如果这个原子变量的值还是value
,我就睡会儿,等它变了再叫醒我。”notify_one()
: 唤醒等待该原子变量的其中一个线程。 就像线程说:“嘿,有一个线程在等这个原子变量,叫醒它!”notify_all()
: 唤醒所有等待该原子变量的线程。 就像线程说:“所有等这个原子变量的线程,都给我起来干活!”
三、 语法详解与注意事项
这三兄弟用法简单粗暴,但也有些需要注意的地方。
-
wait
函数:- 参数:接受一个
value
作为参数,线程会一直休眠,直到原子变量的值不等于这个value
。 - 返回值:无返回值。
- 内部原理:
wait
函数内部会原子地检查原子变量的值,如果等于value
,则将线程置于等待状态,并释放原子变量上的锁(如果有)。当其他线程调用notify_one
或notify_all
唤醒该线程时,线程会重新获取原子变量上的锁,并再次检查原子变量的值。如果仍然等于value
,则线程会再次进入等待状态。 - Spurious Wakeups:
wait
函数可能会发生虚假唤醒(spurious wakeups),也就是说线程可能会被唤醒,但原子变量的值仍然等于value
。因此,在使用wait
函数时,必须在一个循环中进行检查,确保原子变量的值确实发生了改变。
- 参数:接受一个
-
notify_one
函数:- 参数:无参数。
- 返回值:无返回值。
- 作用:唤醒等待该原子变量的其中一个线程。如果有多个线程在等待,则由实现决定唤醒哪个线程。
-
notify_all
函数:- 参数:无参数。
- 返回值:无返回值。
- 作用:唤醒所有等待该原子变量的线程。
四、 代码示例:生产者-消费者模型
咱们用一个经典的生产者-消费者模型来演示这三个函数的使用。
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> data_ready(0); // 0: 数据未准备好, 1: 数据已准备好
std::vector<int> data;
void producer() {
std::cout << "Producer: Preparing data..." << std::endl;
data = {1, 2, 3, 4, 5}; // 生产数据
std::cout << "Producer: Data ready!" << std::endl;
data_ready.store(1); // 设置数据已准备好
data_ready.notify_one(); // 通知消费者线程
}
void consumer() {
std::cout << "Consumer: Waiting for data..." << std::endl;
data_ready.wait(0); // 等待数据准备好
std::cout << "Consumer: Data received!" << std::endl;
for (int value : data) {
std::cout << "Consumer: Processing data: " << value << std::endl;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
在这个例子中:
data_ready
是一个原子变量,用来表示数据是否准备好。producer
线程负责生产数据,并将data_ready
设置为 1,然后调用notify_one
唤醒消费者线程。consumer
线程调用wait(0)
等待data_ready
变为 1,然后处理数据。
五、 进阶用法:带超时的等待
wait
函数还有一个带超时的版本:wait_for
和 wait_until
。
wait_for(timeout_duration)
: 等待一段时间,如果超时则返回false
,否则返回true
。wait_until(timeout_time)
: 等待到指定的时间点,如果超时则返回false
,否则返回true
。
这哥俩儿可以防止线程无限期地等待下去。
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
std::atomic<int> counter(0);
void worker() {
std::cout << "Worker: Waiting for counter to reach 10..." << std::endl;
if (counter.wait_for(0, std::chrono::seconds(5)) == false) {
std::cout << "Worker: Timeout!" << std::endl;
} else {
std::cout << "Worker: Counter reached 10!" << std::endl;
}
}
int main() {
std::thread t(worker);
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Main: Incrementing counter..." << std::endl;
for (int i = 0; i < 10; ++i) {
counter++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
counter.notify_one();
t.join();
return 0;
}
在这个例子中,worker
线程最多等待 5 秒,如果 counter
在 5 秒内没有达到 10,则会输出 "Timeout!"。
六、 适用场景与最佳实践
wait
、notify_one
和 notify_all
适用于以下场景:
- 状态同步:线程需要等待某个状态改变才能继续执行。
- 事件通知:线程需要通知其他线程某个事件已经发生。
- 生产者-消费者模型:生产者线程生产数据,消费者线程消费数据。
- 任务队列:线程池中的线程等待任务,当有新任务到达时,唤醒其中一个或所有线程。
在使用这三个函数时,需要注意以下几点:
- 避免死锁:确保线程在等待时不会持有其他锁,否则可能导致死锁。
- 处理虚假唤醒:
wait
函数可能会发生虚假唤醒,因此必须在一个循环中进行检查。 - 选择合适的通知函数:如果只需要唤醒一个线程,则使用
notify_one
,如果需要唤醒所有线程,则使用notify_all
。 避免不必要的唤醒,从而提高性能。 - 超时机制:在可能出现长时间等待的场景下,使用
wait_for
或wait_until
设置超时时间,防止线程无限期地等待下去。
七、 与条件变量的对比
std::atomic
的 wait
/notify
系列功能,与 std::condition_variable
相比,有什么优势和劣势呢? 让我们用表格的形式来总结一下:
特性 | std::atomic (wait/notify) |
std::condition_variable |
---|---|---|
依赖 | 仅依赖原子变量 | 依赖互斥锁和条件变量 |
性能 | 通常更高(轻量级) | 相对较低(重量级) |
灵活性 | 较低,仅适用于简单状态同步 | 较高,适用于复杂条件等待 |
代码复杂度 | 较低 | 较高 |
适用场景 | 简单状态同步,高性能要求 | 复杂条件等待 |
虚假唤醒处理 | 需要循环检查 | 需要循环检查 |
简单来说,如果你的场景只需要简单的状态同步,并且对性能有较高要求,那么 std::atomic
的 wait
/notify
系列是不错的选择。如果你的场景比较复杂,需要更灵活的条件等待,那么 std::condition_variable
更适合你。
八、 代码示例:任务队列 (线程池简化版)
让我们再来一个例子,实现一个简单的任务队列,模拟线程池的工作方式。
#include <iostream>
#include <thread>
#include <atomic>
#include <queue>
#include <functional>
std::atomic<bool> stop_flag(false);
std::queue<std::function<void()>> task_queue;
std::atomic<int> task_count(0); // 记录队列中任务的数量
void worker_thread() {
while (!stop_flag) {
if (task_count > 0) {
std::function<void()> task;
{
// 从队列中取出一个任务
task = task_queue.front();
task_queue.pop();
task_count--;
}
task(); // 执行任务
} else {
// 没有任务,等待
task_count.wait(0); // 当队列为空时,线程进入等待状态
}
}
}
void add_task(std::function<void()> task) {
task_queue.push(task);
task_count++;
task_count.notify_one(); // 通知一个等待的线程
}
int main() {
std::thread worker1(worker_thread);
std::thread worker2(worker_thread);
// 添加一些任务
add_task([]() { std::cout << "Task 1 executed by thread: " << std::this_thread::get_id() << std::endl; });
add_task([]() { std::cout << "Task 2 executed by thread: " << std::this_thread::get_id() << std::endl; });
add_task([]() { std::cout << "Task 3 executed by thread: " << std::this_thread::get_id() << std::endl; });
std::this_thread::sleep_for(std::chrono::seconds(2)); // 等待一段时间
stop_flag = true;
task_count.notify_all(); // 通知所有线程退出
worker1.join();
worker2.join();
return 0;
}
在这个例子中,task_count
原子变量既用于判断任务队列是否为空,也用作线程同步的信号量。 当任务队列为空时,worker 线程会 wait
在 task_count
上。 当有新的任务加入时,add_task
会 notify_one
唤醒一个等待的线程。
九、 总结与展望
std::atomic
的 wait
、notify_one
和 notify_all
为我们提供了一种更轻量级的线程同步方式,特别是在简单的状态同步场景下,可以显著提高性能。
C++20 的这些新特性,让并发编程变得更加简单和高效。 掌握这些技能,可以让你在多线程的世界里更加游刃有余,写出更健壮、更高效的代码。
当然,并发编程是一个复杂的主题,需要不断学习和实践才能真正掌握。 希望今天的分享能帮助你更好地理解 std::atomic
的新技能,并在实际项目中灵活运用。
就到这里啦,感谢各位的聆听!