如何正确消除C++代码中的竞态条件并解决程序挂起问题?
解决C++多线程队列竞态条件导致的永久挂起问题
问题背景
- 开启
PREPARE_FILE_TO_READ宏编译运行,生成包含N个'A'和'B'字符的v1.dat文件 - 关闭该宏后运行程序,会出现永久挂起,常见最后输出为
TSQueue::pop() A size=0
竞态条件根源
process_data函数存在明显竞态:多个线程先持有锁判断squeue.size()>0,解锁后进入后续代码块执行pop。若某线程取走队列最后一个元素,另一线程执行pop时队列已空,会因condition_variable无限等待导致程序挂起。如果在process_data全程持有锁,虽能避免挂起,但会彻底丧失多线程并发能力。
正确解决方案
核心思路是让线程安全队列(TSQueue)的pop操作本身具备等待非空队列的能力,同时移除process_data中自行判断队列大小的逻辑,直接调用线程安全的pop接口,从根源消除竞态。
1. 重构TSQueue的线程安全pop方法
给TSQueue添加condition_variable,让pop在队列空时自动等待,直到有元素被推入:
#include <queue> #include <mutex> #include <condition_variable> #include <utility> template<typename T> class TSQueue { private: std::queue<T> q; mutable std::mutex mut; std::condition_variable cv; public: void push(T val) { std::lock_guard<std::mutex> lock(mut); q.push(std::move(val)); cv.notify_one(); // 唤醒一个等待的线程 } T pop() { std::unique_lock<std::mutex> lock(mut); // 等待直到队列非空,避免虚假唤醒 cv.wait(lock, [this]() { return !q.empty(); }); T val = std::move(q.front()); q.pop(); return val; } // 保留非阻塞的空判断接口,但不要在process_data中用它做pop的前置条件 bool empty() const { std::lock_guard<std::mutex> lock(mut); return q.empty(); } };
2. 修改process_data函数
移除自行判断队列大小的逻辑,直接调用TSQueue的pop——该方法会自动等待元素,无需额外判断:
void process_data(TSQueue<char>& squeue) { while (true) { char c = squeue.pop(); // 自动等待,直到队列有元素 // 处理字符的业务逻辑 std::cout << "Processed character: " << c << std::endl; // 加入退出条件:比如收到结束标记'\0'时退出循环 if (c == '\0') { break; } } }
3. 补充线程退出机制
由于pop会无限等待,主线程需要给每个工作线程发送退出信号。例如在所有数据推入队列后,给每个线程push一个特殊结束标记(如'\0'):
int main() { const int thread_count = 4; TSQueue<char> squeue; std::vector<std::thread> threads; // 启动工作线程 for (int i = 0; i < thread_count; ++i) { threads.emplace_back(process_data, std::ref(squeue)); } // 从文件读取数据并推入队列(省略文件读取逻辑) // ... // 给每个线程发送退出信号 for (int i = 0; i < thread_count; ++i) { squeue.push('\0'); } // 等待所有线程结束 for (auto& t : threads) { t.join(); } return 0; }
效果说明
修改后,线程仅在pop的等待和弹出阶段持有锁,处理数据的过程完全并发,既解决了竞态导致的挂起问题,又保留了多线程的性能优势。
内容的提问来源于stack exchange,提问作者And
相关产品推荐
相关产品推荐

