C++ std::condition_variable使用示例及共享变量加锁必要性疑问
std::condition_variable 使用示例及常见问题解答
基础使用示例
以下是std::condition_variable的一个简单使用示例:
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock #include <condition_variable> // std::condition_variable #include <chrono> // 原示例隐式依赖的头文件 std::mutex mtx; std::condition_variable cv; int global_status = 0; void print_id(int id) { std::unique_lock<std::mutex> lck(mtx); while (global_status == 0) { cv.wait(lck); } std::cout << "thread " << id << '\n'; } int main() { std::thread threads[10]; for (int i = 0; i < 10; ++i) { threads[i] = std::thread(print_id, i); } std::cout << "Start" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(5)); { std::unique_lock<std::mutex> lck(mtx); global_status = 1; cv.notify_all(); } for (auto& th : threads) th.join(); return 0; }
为什么修改 global_status 时必须加锁?
哪怕只有单个写线程,加锁依然是强制必要的,核心原因有三个:
- 第一是避免数据竞争的未定义行为。global_status是普通的非原子变量,写线程修改它的同时,可能有读线程(所有执行print_id的工作线程)正在读取它的值,没有锁保护的跨线程读写非原子变量,属于C++标准明确规定的「数据竞争」,会直接触发未定义行为,程序运行结果完全不可控。
- 第二是保证内存可见性。现代CPU都有多层缓存,没有锁或者原子操作的内存屏障语义的话,写线程对global_status的修改可能只会存在于它自己的核心缓存里,其他工作线程永远看不到这个修改,就算你发了notify,工作线程醒过来读的还是旧的0值,会再次进入wait状态永远卡住。
- 第三是避免通知丢失的时序问题。如果不加锁,你可能先调用notify_all,之后才修改global_status的值,刚好有工作线程在这两个操作的间隙醒过来(比如之前的虚假唤醒刚结束重新判断条件),这时候读到的global_status还是0,回去继续wait,之后你再修改global_status已经不会再发通知了,这些线程就永远卡着了。
哪怕你把global_status改成std::atomic<int>,依然不能省略这把锁,上述第三种时序问题依然存在,notify和变量修改的顺序没有锁保护的话还是可能乱序,最终导致通知丢失。只要是和condition_variable配合使用的状态变量,不管是不是原子类型,读写都必须在同一个mutex的保护下进行,这是C++标准里对condition_variable使用的强制要求。
内容的提问来源于stack exchange,提问作者Optimus1
相关产品推荐
相关产品推荐

