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

基于Mutex的C++ Blocking Queue实现错误排查求助

仅用Mutex实现C++阻塞队列的代码问题分析

背景

我近期开始学习并发编程,尝试仅使用Mutex实现C++阻塞队列以完成生产者-消费者示例,尚未接触信号量或读写锁,以下是我的实现代码,请帮忙审查指出错误及原因:

// Thread-safe Queue
template<class T>
class BlockingQueue {
    public:
        BlockingQueue() {
            isempty.lock();
        }
        void push(T obj) {
            std::lock_guard<std::mutex> lk(mtx);
            data.push(obj);
            isempty.unlock(); // Queue is no longer empty
        }
        T pop() {
            isempty.lock(); // Acquired lock means I queue is not empty -- or wait until
                        // Calling isempty first so that there is no deadlock if pop is blocked on this
                        // and meanwhile push is called my another thread 
            mtx.lock();
            auto x = data.front();
            data.pop();
            if(!empty()) 
            {
                mtx.unlock(); 
                isempty.unlock(); // Queue is still not empty
            }
            else {
                mtx.unlock();     // Queue is empty don't unlock "isempty"
            }
            return x;
        }
    private:
        bool empty() {
            return data.empty();
        }
        size_t size() {
            return data.size();
        }
        std::queue<T> data;
        std::mutex mtx; // Mutex to stop data-race
        std::mutex isempty; // Mutex to block on empty Queue
};

代码中的错误及原因

1. isempty锁的核心逻辑完全错误

  • 构造函数中直接调用isempty.lock():队列初始化时本身为空,此时持有锁会导致所有消费者线程一开始就被阻塞,且后续push中的unlock操作违反互斥锁规则——只有持有锁的线程才能调用unlock,无锁状态下调用unlock属于未定义行为,会引发程序崩溃或异常。
  • 多消费者场景失效:当队列中有多个元素时,一个push只能unlock一次,仅能唤醒一个消费者线程,其余消费者会持续阻塞,无法处理队列剩余元素。
  • 空状态维护逻辑颠倒:队列被pop至空时,代码保留isempty锁不释放,但后续push调用unlock时,若此时无线程持有该锁,会触发未定义行为,且后续消费者会一直阻塞,即使队列已被重新填充。

2. 锁顺序错误引发死锁风险

pop函数中先锁isempty再锁mtx,若存在其他线程(如错误操作)先锁mtx再尝试锁isempty,会出现死锁——两个线程互相持有对方需要的锁,无限等待。

3. 数据竞争导致无效状态判断

pop中调用empty()时未持有mtx锁,data.empty()并非线程安全操作,此时其他线程可能正在修改data,导致empty()的结果无效,进而引发后续逻辑错误(如队列实际为空时调用data.front(),触发未定义行为)。

4. 手动解锁存在异常安全问题

pop中手动调用mtx.unlock(),若data.front()或data.pop()抛出异常,mtx会一直被持有,导致其他线程无法访问队列,违反异常安全原则。应使用std::lock_guard或std::unique_lock自动管理锁的释放,避免手动解锁的风险。

5. 竞态条件引发错误唤醒

pop中判断!empty()后再解锁isempty,但在判断与解锁之间,可能有其他线程执行pop操作让队列变为空,此时解锁isempty会唤醒不必要的消费者线程,这些线程拿到锁后发现队列为空,会触发data.front()的未定义行为。

修正后的简化实现(仅用Mutex)

如果坚持只用Mutex实现,可通过循环检查队列状态的方式模拟阻塞逻辑,避免上述问题:

#include <queue>
#include <mutex>
#include <utility> // 用于std::move

template<class T>
class BlockingQueue {
public:
    BlockingQueue() = default;

    void push(T obj) {
        std::lock_guard<std::mutex> lk(mtx);
        data.push(std::move(obj));
        // 唤醒一个等待的线程
        wait_lock.unlock();
    }

    T pop() {
        while (true) {
            wait_lock.lock();
            std::lock_guard<std::mutex> lk(mtx);
            if (!data.empty()) {
                auto x = std::move(data.front());
                data.pop();
                // 队列还有元素,解锁让其他消费者可以进入
                if (!data.empty()) {
                    wait_lock.unlock();
                }
                return x;
            }
            wait_lock.unlock();
        }
    }

private:
    std::queue<T> data;
    std::mutex mtx;          // 保护队列数据的互斥锁
    std::mutex wait_lock;    // 模拟等待/通知的锁,初始为解锁状态
};

内容的提问来源于stack exchange,提问作者Schon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 19:03:24