C++ `std::atomic` 线程同步库的高级用法:`wait`, `notify_one`, `notify_all` (C++20)

哈喽,各位好!

今天咱们来聊聊 C++20 引入的 std::atomic 的新技能:waitnotify_onenotify_all。这哥仨儿的加入,让原子变量在线程同步方面更加游刃有余,简直是原子操作界的“完全体”。

一、 背景故事:原子变量的自我修养

在并发编程的世界里,共享数据是最容易引发混乱的根源。多个线程同时访问和修改同一块内存,就可能导致数据竞争,程序崩溃,或者出现一些神鬼莫测的bug。为了解决这个问题,C++ 提供了 std::atomic,它能保证对原子变量的操作是原子性的,也就是不可分割的。

但是,仅仅保证原子性还不够。有时候,我们需要线程之间能够协调工作,比如一个线程需要等待某个条件成立才能继续执行,或者一个线程需要通知其他线程某个事件已经发生。在 C++20 之前,我们通常需要借助互斥锁、条件变量等更重量级的工具才能实现这些功能。

而现在,有了 waitnotify_onenotify_all,原子变量也能胜任这些任务了。

二、 三剑客登场:wait, notify_one, notify_all

这三个函数,就像是原子变量的“睡眠”、“叫醒”和“集体起床”功能。

  • wait(value): 让线程休眠,直到原子变量的值发生改变,并且不等于 value。 这就像线程说:“如果这个原子变量的值还是 value,我就睡会儿,等它变了再叫醒我。”
  • notify_one(): 唤醒等待该原子变量的其中一个线程。 就像线程说:“嘿,有一个线程在等这个原子变量,叫醒它!”
  • notify_all(): 唤醒所有等待该原子变量的线程。 就像线程说:“所有等这个原子变量的线程,都给我起来干活!”

三、 语法详解与注意事项

这三兄弟用法简单粗暴,但也有些需要注意的地方。

  • wait 函数

    • 参数:接受一个 value 作为参数,线程会一直休眠,直到原子变量的值不等于这个 value
    • 返回值:无返回值。
    • 内部原理wait 函数内部会原子地检查原子变量的值,如果等于 value,则将线程置于等待状态,并释放原子变量上的锁(如果有)。当其他线程调用 notify_onenotify_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_forwait_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!"。

六、 适用场景与最佳实践

waitnotify_onenotify_all 适用于以下场景:

  • 状态同步:线程需要等待某个状态改变才能继续执行。
  • 事件通知:线程需要通知其他线程某个事件已经发生。
  • 生产者-消费者模型:生产者线程生产数据,消费者线程消费数据。
  • 任务队列:线程池中的线程等待任务,当有新任务到达时,唤醒其中一个或所有线程。

在使用这三个函数时,需要注意以下几点:

  • 避免死锁:确保线程在等待时不会持有其他锁,否则可能导致死锁。
  • 处理虚假唤醒wait 函数可能会发生虚假唤醒,因此必须在一个循环中进行检查。
  • 选择合适的通知函数:如果只需要唤醒一个线程,则使用 notify_one,如果需要唤醒所有线程,则使用 notify_all。 避免不必要的唤醒,从而提高性能。
  • 超时机制:在可能出现长时间等待的场景下,使用 wait_forwait_until 设置超时时间,防止线程无限期地等待下去。

七、 与条件变量的对比

std::atomicwait/notify 系列功能,与 std::condition_variable 相比,有什么优势和劣势呢? 让我们用表格的形式来总结一下:

特性 std::atomic (wait/notify) std::condition_variable
依赖 仅依赖原子变量 依赖互斥锁和条件变量
性能 通常更高(轻量级) 相对较低(重量级)
灵活性 较低,仅适用于简单状态同步 较高,适用于复杂条件等待
代码复杂度 较低 较高
适用场景 简单状态同步,高性能要求 复杂条件等待
虚假唤醒处理 需要循环检查 需要循环检查

简单来说,如果你的场景只需要简单的状态同步,并且对性能有较高要求,那么 std::atomicwait/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 线程会 waittask_count 上。 当有新的任务加入时,add_tasknotify_one 唤醒一个等待的线程。

九、 总结与展望

std::atomicwaitnotify_onenotify_all 为我们提供了一种更轻量级的线程同步方式,特别是在简单的状态同步场景下,可以显著提高性能。

C++20 的这些新特性,让并发编程变得更加简单和高效。 掌握这些技能,可以让你在多线程的世界里更加游刃有余,写出更健壮、更高效的代码。

当然,并发编程是一个复杂的主题,需要不断学习和实践才能真正掌握。 希望今天的分享能帮助你更好地理解 std::atomic 的新技能,并在实际项目中灵活运用。

就到这里啦,感谢各位的聆听!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注