2

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

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

4

1 回答 1

4

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

正如你在这种情况下所做的那样是错误的。您应该使用该成员:

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

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

另请注意,您必须使用while而不是if围绕调用QWaitCondition::wait. 这是出于(可能的)虚假唤醒的复杂原因 - Qt 文档在此处不清楚。但更重要的是,互斥锁的唤醒和随后的重新获取不是原子操作,这意味着您必须重新检查变量队列是否为空。这可能是您之前遇到死锁/UB 的最后一种情况。

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

于 2020-08-18T00:08:48.813 回答