Qt:当多个排队信号调用同一个槽时如何避免死锁
Qt: How to avoid deadlock when multiple queued signals invoke same slot
在下面的代码中,我在 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();
}
有一个线程将发出三个信号,所有信号都排队连接到 class A 的实例,并且所有 class A' 槽将调用到 someOperation
,并且 someOperation
受互斥锁保护,它会弹出一个消息框。
Qt::QueuedConnection 2 The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread.
好像slot2是在slot1的消息框还在主线程模态的时候调用的,但是那个时候slot1有锁mutex
,所以死锁。
如何修改代码避免死锁?
更新:(2019 年 1 月 17 日)
我想要存档的是:slot2 在 slot1 完成之前不被执行。
应该保留的是:
- worker是处理数据的后台线程,耗时长;所以,无论如何,这三个信号将从其他线程发出。
- 工人不应该被发射信号阻挡。
- 插槽应该在主线程中执行,因为它们会更新 GUI。
someOperation
不可重入。
那是因为你的函数 void someOperation()
不是 reentrant。
QMessageBox
的静态函数跨越它们自己的事件循环,重复调用 QCoreApplication::processEvents()
:
- 第一次调用
someOperation()
的执行卡在 QMessageBox::warning(...)
。
- 在那里,
exec()
调用 processEvents()
,3. 看到第二个信号
- 并再次调用
someOperation()
- 尝试重新锁定
mutex
失败。
如何解决这个取决于你想达到什么目的...
关于您 QThread
的一般方法:You're doing it wrong.
(link 为主题提供了良好的开端,但不是完整的解决方案。)
您创建并启动了一个后台线程。但是那个线程只会发出三个信号然后完成。
插槽将在主 (GUI) 事件循环中调用,因为那是 A *a
的 thread affinity。
要让插槽在后台执行,您需要:
- 创建没有父级的 A 实例:
A *a = new A();
- 创建您的 Worker 实例,并将该应用程序作为父级:
Worker *w = new Worker(&app);
(或者什么都不做,至少不使用 a
)
- 更改 A 实例的线程关联:
a->moveToThread(Worker);
- 不要覆盖
Worker::run()
,或者如果您确实想要(参见第 5 点),请调用基本实现:QThread::run();
- 从 main 发出信号(您可以从 运行() 发出它们,但这不是必需的)。
要求"someOperation is not reentrant"
为奇数。如果尝试重新进入,会发生什么?鉴于 someOperation
只能从 main
线程调用,我只能看到两个选项...
- 完全阻止 mutex/barrier 等,如您所试。
- 基于递归级别计数器进行阻止并旋转事件循环,直到该计数器递减为零。
1) 将阻塞线程的事件循环,完全阻止当前消息对话框正常运行。
2) 将同时允许所有消息对话框,而不是序列化它们。
与其尝试使 someOperation
不可重入,我认为您需要确保以不会导致重入的方式使用。
一个选择可能是使用单独的 QObject
派生的 class 实例 QThread
。考虑以下...
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();
我只进行了基本测试,但它似乎提供了所需的行为。
在下面的代码中,我在 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();
}
有一个线程将发出三个信号,所有信号都排队连接到 class A 的实例,并且所有 class A' 槽将调用到 someOperation
,并且 someOperation
受互斥锁保护,它会弹出一个消息框。
Qt::QueuedConnection 2 The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread.
好像slot2是在slot1的消息框还在主线程模态的时候调用的,但是那个时候slot1有锁mutex
,所以死锁。
如何修改代码避免死锁?
更新:(2019 年 1 月 17 日)
我想要存档的是:slot2 在 slot1 完成之前不被执行。
应该保留的是:
- worker是处理数据的后台线程,耗时长;所以,无论如何,这三个信号将从其他线程发出。
- 工人不应该被发射信号阻挡。
- 插槽应该在主线程中执行,因为它们会更新 GUI。
someOperation
不可重入。
那是因为你的函数 void someOperation()
不是 reentrant。
QMessageBox
的静态函数跨越它们自己的事件循环,重复调用 QCoreApplication::processEvents()
:
- 第一次调用
someOperation()
的执行卡在QMessageBox::warning(...)
。 - 在那里,
exec()
调用processEvents()
,3. 看到第二个信号 - 并再次调用
someOperation()
- 尝试重新锁定
mutex
失败。
如何解决这个取决于你想达到什么目的...
关于您 QThread
的一般方法:You're doing it wrong.
(link 为主题提供了良好的开端,但不是完整的解决方案。)
您创建并启动了一个后台线程。但是那个线程只会发出三个信号然后完成。
插槽将在主 (GUI) 事件循环中调用,因为那是 A *a
的 thread affinity。
要让插槽在后台执行,您需要:
- 创建没有父级的 A 实例:
A *a = new A();
- 创建您的 Worker 实例,并将该应用程序作为父级:
Worker *w = new Worker(&app);
(或者什么都不做,至少不使用a
) - 更改 A 实例的线程关联:
a->moveToThread(Worker);
- 不要覆盖
Worker::run()
,或者如果您确实想要(参见第 5 点),请调用基本实现:QThread::run();
- 从 main 发出信号(您可以从 运行() 发出它们,但这不是必需的)。
要求"someOperation is not reentrant"
为奇数。如果尝试重新进入,会发生什么?鉴于 someOperation
只能从 main
线程调用,我只能看到两个选项...
- 完全阻止 mutex/barrier 等,如您所试。
- 基于递归级别计数器进行阻止并旋转事件循环,直到该计数器递减为零。
1) 将阻塞线程的事件循环,完全阻止当前消息对话框正常运行。
2) 将同时允许所有消息对话框,而不是序列化它们。
与其尝试使 someOperation
不可重入,我认为您需要确保以不会导致重入的方式使用。
一个选择可能是使用单独的 QObject
派生的 class 实例 QThread
。考虑以下...
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();
我只进行了基本测试,但它似乎提供了所需的行为。