TaskRunner实现中条件变量信号发送的互斥量锁定要求与Run函数is_running_读取正确性的技术疑问
先简单回顾下背景:我们实现了一个多线程TaskRunner组件,最初用轮询方式处理任务队列,后来引入条件变量优化轮询逻辑,但现在有两个线程安全相关的疑问需要澄清。
问题1:Stop()中调用m_task_cond_var.notify_one()前是否需要锁定m_task_mutex?
答案是不需要,这样做是安全的。
从C++标准的角度来说,条件变量的notify_one()/notify_all()操作本身是线程安全的,并不要求调用者必须持有关联的互斥量。你之所以有疑惑,可能是看到过很多场景下会在notify前加锁,但那通常是为了避免“丢失唤醒”的极端场景——比如当你修改等待条件的同时,线程刚好进入wait状态。不过在你的代码中:
is_running_的修改已经被is_running_mutex_保护,确保了内存可见性(其他线程能看到更新后的is_running_值);- 即使在调用
notify_one()时工作线程还没进入wait,当它下次检查wait的条件(!is_running_ || !tasks_queue_.empty())时,也会发现is_running_已经为false,从而退出循环。
所以完全不需要在Stop()中额外锁定m_task_mutex,当前的实现是没问题的。
问题2:Run()工作线程中直接读取is_running_而未锁定is_running_mutex_,这种做法是否正确?
这种做法不正确,会导致数据竞争,属于C++标准中的未定义行为。
原因在于:is_running_是非原子变量,且Stop()中修改它时持有is_running_mutex_,但Run()中读取它时没有同步机制(既没锁is_running_mutex_,也不是原子操作)。多线程下对非原子变量的无同步读写会触发数据竞争,可能导致工作线程读取到is_running_的旧值,甚至出现不可预测的行为。
修复方案有两种:
将
is_running_改为原子变量
把is_running_声明为std::atomic<bool>,这样所有读写操作都是原子的,自带内存同步语义,不需要再用is_running_mutex_保护:std::atomic<bool> is_running_ = false;这种方案更简洁高效,是推荐的做法。
读取
is_running_时始终锁定is_running_mutex_
在Run()中所有读取is_running_的地方(包括wait的条件判断、循环条件)都加上锁:void TaskRunnerImpl::Run() { while(true) { // 先检查是否继续运行 { std::lock_guard<std::mutex> lock(is_running_mutex_); if(!is_running_) { return; } } Task* task_to_run = nullptr; { std::unique_lock<std::mutex> mlock(m_task_mutex); // 条件判断中也要确保is_running_的读取是同步的 m_task_cond_var.wait(mlock, [this]() { std::lock_guard<std::mutex> lock(is_running_mutex_); return !(is_running_ && tasks_queue_.empty()); }); { std::lock_guard<std::mutex> lock(is_running_mutex_); if(!is_running_) { return; } } if(!tasks_queue_.empty()) { task_to_run = tasks_queue_.front(); task_to_run->Run(); tasks_queue_.pop_front(); } } if(task_to_run) delete task_to_run; } }这种方案会增加锁的开销,代码也更繁琐,不如原子变量方案优雅。
内容的提问来源于stack exchange,提问作者kadina

