You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确消除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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 05:07:35