《C++ Concurrency in Action》第6章线程安全队列的条件变量用法是否有误?
分析《C++ Concurrency in Action》第6章线程安全队列的条件变量问题
嘿,你观察得很仔细!这段代码里的条件变量用法确实存在线程安全问题,咱们一步步拆解来看:
首先先把你给出的代码片段结合原书逻辑补全,方便分析:
// 假设队列的核心成员与节点结构如下 struct node { std::shared_ptr<T> data; std::unique_ptr<node> next; }; std::unique_ptr<node> head; node* tail; std::mutex head_mutex; std::mutex tail_mutex; std::condition_variable data_cond; std::unique_lock<std::mutex> wait_for_data() { std::unique_lock<std::mutex> head_lock(head_mutex); data_cond.wait(head_lock, [&] {return head.get() != queue::get_tail(); }); return std::move(head_lock); } void push(T new_value) { std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value))); std::lock_guard<std::mutex> tail_lock(tail_mutex); auto const old_tail = tail; old_tail->next = std::make_unique<node>(); tail = old_tail->next.get(); tail->data = new_data; data_cond.notify_one(); } // 问题根源所在的get_tail实现 node* queue::get_tail() { return tail; // 直接访问受tail_mutex保护的成员,未加锁 }
核心问题:谓词中的共享数据未受保护
这段代码的致命问题在于条件变量的谓词里访问了未加锁的共享数据:
tail成员是被tail_mutex专属保护的,只有持有tail_mutex的线程才能安全读写它。- 但在
wait_for_data的lambda谓词中,queue::get_tail()直接返回tail的值,此时当前线程只持有head_mutex,完全没有获取tail_mutex——这就构成了数据竞争,会导致未定义行为,比如读到tail的过时值,或者在多线程并发修改时出现内存访问错误。
直白点说:当另一个线程正在执行push修改tail时,wait_for_data里的线程在无锁状态下读取tail,这是线程安全的大忌。
修正方案:在谓词中安全访问共享数据
要解决这个问题,必须保证在谓词中访问tail时持有对应的tail_mutex。同时要注意加锁顺序的一致性(所有需要同时持有两个锁的地方,都先拿head_mutex再拿tail_mutex),避免死锁。
修改后的wait_for_data可以这样写:
std::unique_lock<std::mutex> wait_for_data() { std::unique_lock<std::mutex> head_lock(head_mutex); data_cond.wait(head_lock, [&] { // 在判断时临时获取tail_mutex,保证访问tail的线程安全 std::lock_guard<std::mutex> tail_lock(tail_mutex); return head.get() != tail; }); return std::move(head_lock); }
这样每次条件变量唤醒后检查谓词时,都会先持有tail_mutex再访问tail,彻底消除了数据竞争的风险。虽然每次检查都要加解锁tail_mutex会有一点性能开销,但这是保证线程安全的必要代价。
另外补充:原书里的实现其实用了哑节点设计,判断队列非空的逻辑应该是head->next != nullptr,可能你截取的代码片段有遗漏,但核心的线程安全问题还是在于未加锁访问受保护的共享数据这一点。
内容的提问来源于stack exchange,提问作者olist
相关产品推荐
相关产品推荐

