Qt QTcpSocket:如何在readyRead信号中避免死锁?

3
我需要在Windows 7中使用Qt的一些帮助。似乎Qt的 readyRead() 信号是由异步过程调用发出的,这会导致代码在相同的线程中并发执行。
例如,我有一个队列,应该由 DoRead() 和在锁定下访问的 DoTimer() 访问,整个操作在 UI(主)线程中运行。但是,有时会发生死锁,因为调用 DoRead() 。代码将停止在 DoRead() 处执行。如果显示消息框,则可以复制死锁。但是,我惊讶地发现, OnRead() 仍然是同时调用的。对我来说唯一的解释是, OnRead() 由Windows APC调用。
请参阅MSDN文章 Asynchronus Procedure Calls

异步过程调用(APC)是在特定线程的上下文中异步执行的函数。当将APC排队到线程时,系统会发出软件中断。 线程下次调度时,它将运行APC函数

我的假设是 readyRead() 可能是一个APC,这是正确的吗?
在任一情况下,我该怎么做才能防止死锁?我需要在 DoRead() 中访问队列以填充队列,并在 DoTimer() (当然还有其他方法)中读取、写入或删除来自同一队列的条目。递归互斥锁不是解决方案,因为两个调用都发生在同一线程中。
class QMySocket : public QTcpSocket {
public:
    QMySocket() {
        ...
        connect(this, SIGNAL(readyRead()), this, SLOT(DoRead()));
        connect(_MyTimer, SIGNAL(timeout()), this, SLOT(DoTimer()));
        ...
    }
private:
    QTimer* _MyTimer;
    QQueue<int> _MyQueue;
    QMutex _Lock;

    void DoRead() {
        _Lock.lock(); // <-- Dead Lock here (same Thread ID as in DoTimer)
        _MyQueue... // Do some queue operation
        // DoSomething
        _Lock.unlock();
    }

    void DoTimer() {
        _Lock.lock();
        QQueue<int>::iterator i = _MyQueue.begin();
        while (i != _MyQueue.end()) { // Begin queue operation
            if (Condition) {
                QMessageBox::critical(...);
                i = _MyQueue.erase(i);
            } else {
                i++;
            }
        } // end queue operation
        _Lock.unlock();
    }
};

编辑2:我发现这与APC无关。问题只是由QMessageBox创建的额外消息循环。

不要直接调用QMessageBox,所有消息将被排队,并在任何队列操作后显示。

void DoTimer() {
    QList<QString> Messages;
    QQueue<int>::iterator i = _MyQueue.begin();
    while (i != _MyQueue.end()) { // Begin queue operation
        if (Condition) {
            Messages.append(...);
            i = _MyQueue.erase(i);
        } else {
            i++;
        }
    } // end queue operation
    QMessageBox::critical(Messages);
}

如果队列没有并发访问(没有多线程),则不需要锁定。

你说你有一个线程;通过死锁,你是指应用程序冻结了吗?我假设你已经通过lock()语句,并且你被“中断”并跳转到DoRead(),这需要先调用unlock(),所以你被卡住了(在这个例子中)在DoRead()的lock()上。我的解释正确吗? - Adrian
1
readyRead()只是一个异步发送的信号;每当它被发送时,就会执行DoRead()。 - Adrian
1
我不明白为什么在这里要使用互斥锁,因为所有内容都在单个线程中执行。例如,您可以在类“readDone”内部发出信号,然后将其连接到“DoTimer”,或直接在“DoRead”的末尾调用“DoTimer”。 - Neox
@Adrian:确切地说,应用程序会冻结,因为在DoTimer()中已经进入了锁定状态。即使在单线程应用程序中,readyRead()似乎也是异步的(看起来像APC调用)。现在的问题是,如何保护队列免受并发访问?锁定正是为并发访问而设计的,但在这种情况下,这不是解决方案,因为我们实际上并没有并发访问,仍然只有一个线程。但是,一旦DoRead等待锁定,DoTimer将永远不会再次获得处理器时间,因此锁定永远无法释放,DoRead将永远等待。 - bkausbk
只需移除锁定即可,您没有并发访问。这是一个单线程应用程序。 - Adrian
显示剩余2条评论
1个回答

2
您唯一的问题是对以下内容的调用:
QMessageBox::critical(...);

此调用将阻塞,直到您按下按钮。但是由于您在仍持有锁时调用了它,因此您的DoRead死锁。

没有任何理由在持有该锁时打开消息框!

如果您仍希望在显示消息框的同时使DoTimer响应,请不要使用QMessageBox :: critical等静态便捷方法。

最好这样做:

   // Somewhere in the constructor ...
   QMessageBox* msgBox = new QMessageBox( this );
   msgBox->setAttribute( QWidget::WA_DeleteOnClose );
   msgBox->setStandardButtons( QMessageBox::Ok );
   msgBox->setWindowTitle( tr("Error") );
   msgBox->setModal( true );
   //...

void DoTimer() {
    _Lock.lock();
    // DoSomething
    _MyQueue... // Iterate over queue, and do some queue operation (delete entires for exmaple)
    _Lock.unlock();
    msgBox->setText( tr("DingDong!") );
    if (!msgBox->isVisible())
        msgBox->open( this, SLOT(msgBoxClosed(QAbstractButton*)) );
}

void MyWidget::msgBoxClosed(QAbstractButton*) {
   qDebug("Byebye msgbox");
}

但从您的代码中,我并没有看到使用互斥锁的理由。 没有并发,对吗?


是的,谢谢你的提示。我昨天也发现了,调用消息框是问题所在。我使用锁是因为最初计划使用多个线程。调用消息框只是举例而已。在我的真实代码中,情况更糟,因为在这个锁定的部分中,我发出了一个Qt信号。我永远无法控制是否有人连接到该信号并进入可警报的等待状态(这至少会导致Windows平台上的死锁)。但是,在持有锁或在循环中使用迭代器进行某些队列操作时,解决方案不是发出任何内容。 - bkausbk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接