条件变量与本地互斥锁的奇怪使用

And odd use of conditional variable with local mutex

仔细研究旧的大型项目的遗留代码,我发现使用了一些奇怪的方法来创建线程安全队列,如下所示:

template < typename _Msg>
class WaitQue: public QWaitCondition 
{
public:
    typedef _Msg  DataType;

    void wakeOne(const DataType& msg) 
    {
        QMutexLocker lock_(&mx);
        que.push(msg);
        QWaitCondition::wakeOne(); 
    }
    
    void wait(DataType& msg)
    {
        /// wait if empty.
        {
            QMutex wx;  // WHAT?
            QMutexLocker cvlock_(&wx);
            if (que.empty()) 
                QWaitCondition::wait(&wx);  
        }

        {
            QMutexLocker _wlock(&mx);
            msg = que.front();            
            que.pop();
        }
    }

    unsigned long size() { 
        QMutexLocker lock_(&mx); 
        return que.size();
    }

private:
    std::queue<DataType> que;

    QMutex mx;
};

wakeOne 从线程中用作一种“发布”功能,wait 从其他线程中调用并无限期地等待,直到消息出现在队列中。在某些情况下,线程之间的角色相反在不同的阶段并使用单独的队列。

这是通过创建本地 QMutex 使用 QMutex 的合法方式吗?我有点理解为什么有人可以在读取 que 的大小时这样做来避免死锁,但它是如何工作的呢?有没有更简单、更惯用的方法来实现这种行为?

拥有局部条件变量是合法的。但它通常没有意义。

正如你在这种情况下的计算是错误的。您应该使用成员:

void wait(DataType& msg)
{
    QMutexLocker cvlock_(&mx);
    while (que.empty()) 
        QWaitCondition::wait(&mx);

    msg = que.front();            
    que.pop();
}

另请注意,在 QWaitCondition::wait 的调用周围必须使用 while 而不是 if。这是出于关于(可能的)虚假唤醒的复杂原因——Qt 文档在这里并不清楚。但更重要的是,唤醒和随后重新获取互斥锁不是原子操作,这意味着您必须重新检查变量队列是否为空。这可能是您之前获得 deadlocks/UB.

的最后一个案例

考虑一个空队列和一个调用者(线程 1)wait 进入 QWaitCondition::wait 的场景。该线程阻塞。然后线程 2 出现并向队列添加一个项目并调用 wakeOne。线程 1 被唤醒并尝试重新获取互斥量。但是,线程 3 在您的 wait 实现中出现,在线程 1 之前获取互斥锁,看到队列不为空,处理单个项目并继续前进,释放互斥锁。然后被唤醒的线程 1 最终从 QWaitCondition::wait 获取互斥体 returns 并尝试处理...一个空队列。哎呀。