【问题标题】:Qt: How to avoid deadlock when multiple queued signals invoke same slotQt:当多个排队信号调用同一个槽时如何避免死锁
【发布时间】:2019-01-16 02:16:39
【问题描述】:

在以下代码中,我遇到了someOperation 中的死锁:

class A : public QObject {
    Q_OBJECT
public:
    explicit A(QObject* parent) : QObject(parent), data(0) {}
public slots:
    void slot1() {
        someOperation();
    }
    void slot2() {
        someOperation();
    }
    void slot3() {
        someOperation();
    }
private:
    void someOperation() {
        QMutexLocker lk(&mutex);
        data++;
        QMessageBox::warning(NULL, "warning", "warning");
        data--;
        assert(data == 0);
    }
    int data;
    QMutex mutex; //protect data
};

class Worker: public QThread {
    Q_OBJECT
public:
    explicit Worker(QObject* parent) : QThread(parent) {}
protected:
    virtual void run() {
        // some complicated data processing
        emit signal1();
        // other complicated data processing
        emit signal2();
        // much complicated data processing
        emit signal3();
        qDebug() << "end run";
    }
signals:
    void signal1();
    void signal2();
    void signal3();

};

int main(int argc, char *argv[])
{
        QApplication app(argc, argv);
        A* a = new A(&app);
        Worker* w = new Worker(a);
        QObject::connect(w, SIGNAL(signal1()), a, SLOT(slot1()), Qt::QueuedConnection);
        QObject::connect(w, SIGNAL(signal2()), a, SLOT(slot2()), Qt::QueuedConnection);
        QObject::connect(w, SIGNAL(signal3()), a, SLOT(slot3()), Qt::QueuedConnection);
        w->start();

        return app.exec();
}

有一个线程会发出三个信号,它们都排队连接到A类的一个实例,并且所有A类的槽都会调用someOperationsomeOperation被互斥锁保护,它会弹出一个消息框。

Qt::QueuedConnection 2 当控制返回到接收者线程的事件循环时调用槽。该槽在接收者的线程中执行。

当 slot1 的消息框在主线程中仍在执行模式时,似乎调用了 slot2,但当时 slot1 已锁定 mutex,所以死锁。

如何修改代码避免死锁?

更新:(2019 年 1 月 17 日)

我想要存档的是:在 slot1 完成之前不能执行 slot2。

应该保留的有:

  1. worker是后台线程处理数据,耗时较长;所以,无论如何,这三个信号将从其他线程发出。
  2. worker 不应通过发出信号来阻塞。
  3. slots 应该在主线程中执行,因为它们会更新 GUI。
  4. someOperation 不可重入。

【问题讨论】:

  • 我会考虑是否可以在需要查询用户的代码行将后台操作分成两个槽。然后你会在前半部分结束时发出一个信号,这将在 GUI 线程中触发一个消息框对话框,你可以简单地将函数的后半部分连接到对话框的完成信号。

标签: qt qthread qtconcurrent


【解决方案1】:

"someOperation is not reentrant" 的要求是奇数。如果尝试重入会发生什么?鉴于someOperation 只能从main 线程调用,我只能看到两个选项...

  1. 如您所尝试的那样,使用互斥锁/屏障等完全阻止。
  2. 基于递归级别计数器进行阻塞并旋转事件循环,直到该计数器减至零。

1) 将阻塞线程的事件循环,完全阻止当前消息对话框正常运行。

2) 将同时允许所有消息对话框,而不是序列化它们。

与其试图让someOperation 不可重入,我认为您需要确保以不会导致重入的方式使用。

一种选择可能是在其自己的QThread 上使用单独的QObject 派生类实例。考虑以下...

class signal_serialiser: public QObject {
  Q_OBJECT;
signals:
  void signal1();
  void signal2();
  void signal3();
};

如果signal_serialiser 的一个实例被移动到它自己的线程中,它可以充当一个队列来缓冲和转发各种信号(如果使用了合适的连接类型)。在您当前的代码中...

QObject::connect(w, SIGNAL(signal1()), a, SLOT(slot1()), Qt::QueuedConnection);
QObject::connect(w, SIGNAL(signal2()), a, SLOT(slot2()), Qt::QueuedConnection);
QObject::connect(w, SIGNAL(signal3()), a, SLOT(slot3()), Qt::QueuedConnection);

把它改成...

signal_serialiser signal_serialiser;
QObject::connect(w, SIGNAL(signal1()), &signal_serialiser, SIGNAL(signal1()));
QObject::connect(w, SIGNAL(signal2()), &signal_serialiser, SIGNAL(signal2()));
QObject::connect(w, SIGNAL(signal3()), &signal_serialiser, SIGNAL(signal3()));

/*
 * Note the use of Qt::BlockingQueuedConnection for the
 * signal_serialiser --> A connections.
 */
QObject::connect(&signal_serialiser, SIGNAL(signal1()), a, SLOT(slot1()), Qt::BlockingQueuedConnection);
QObject::connect(&signal_serialiser, SIGNAL(signal2()), a, SLOT(slot2()), Qt::BlockingQueuedConnection);
QObject::connect(&signal_serialiser, SIGNAL(signal3()), a, SLOT(slot3()), Qt::BlockingQueuedConnection);
QThread signal_serialiser_thread;
signal_serialiser.moveToThread(&signal_serialiser_thread);
signal_serialiser_thread.start();

我只进行了基本测试,但它似乎给出了所需的行为。

【讨论】:

    【解决方案2】:

    那是因为你的函数void someOperation() 不是reentrant

    QMessageBox 的静态函数跨越自己的事件循环,它反复调用QCoreApplication::processEvents()

    1. someOperation() 的第一次调用在 QMessageBox::warning(...) 处被卡住。
    2. 在那里,exec() 调用 processEvents(),3. 它看到了第二个信号
    3. 并再次调用someOperation()
    4. 尝试重新锁定 mutex 失败的地方。

    如何解决这个问题取决于您想要实现的目标......


    关于您对QThread 的一般处理方法:You're doing it wrong.
    (该链接为主题提供了一个良好的开端,但不是一个完整的解决方案。)

    您创建并启动一个后台线程。但是那个线程只会发出三个信号然后结束。

    插槽将在主 (GUI) 事件循环中调用,因为那是您的 A *athread affinity

    要让插槽在后台执行,您需要:

    1. 创建没有父级的 A 实例:A *a = new A();
    2. 以应用为父级创建您的 Worker 实例:Worker *w = new Worker(&amp;app);(或什么都不带,至少不带 a
    3. 更改 A 实例的线程亲和性:a-&gt;moveToThread(Worker);
    4. 不要覆盖Worker::run(),或者如果你真的想要(见第5点),调用基本实现:QThread::run();
    5. 从 main 发出信号(您可以从 run() 发出它们,但这不是必需的)。

    【讨论】:

    • 不幸的是,上述建议(如果我没看错的话)会导致QMessageBox::warning 调用在与main 关联的线程之外的线程上运行。 Qt 不支持。
    • @G.M.我在这里解决两个独立的问题。第一个是 QMessageBox::staticCall() 是死锁的原因。为了避免这种情况,我们需要知道 OP 想要 实现什么。第二点是QThread的错误使用。当然,解决第二个问题并不能解决第一个问题。
    • 我不确定我是否同意导致僵局的原因。我怀疑真正的原因是线程试图重新锁定它已经拥有的non-recursive QMutex。我同意这个问题需要澄清。
    • @G.M.当然,死锁发生在重新锁定 QMutex 时。但这是因为someOperation() 的第一次调用的执行卡在QMessageBox::warning(...),exec() 调用processEvents(),它看到第二个信号并再次调用someOperation(),尝试重新锁定失败。
    • 好的,我很困惑,因为您的回答没有明确提到互斥锁是导致死锁的原因。
    猜你喜欢
    • 2021-12-06
    • 2019-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-07
    • 1970-01-01
    • 2023-01-29
    • 2017-01-05
    相关资源
    最近更新 更多