QT 信号和插槽在单线程应用程序中的直接连接行为
QT signals and slots direct connection behaviour in application with a single thread
我很难理解什么时候会发生什么。两个不同的信号连接到两个不同的插槽,当一个插槽未完成时,将发出另一个插槽的信号(对于直接连接到各自信号的两个插槽),其中应用程序只有“一个”线程。
本文来自QT官方文档:
Direct Connection: The slot is invoked immediately, when the signal is emitted. The slot is executed in the emitter's thread, which is not necessarily the receiver's thread.
Queued Connection: 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.
与排队连接不同,直接连接说的是“立即”。这是否意味着,如果在第一个插槽尚未完成时发出第二个信号,则第一个插槽将被中断,并且即使应用程序是单线程应用程序,它也会 运行 与第二个插槽并发?如果是这样,为什么我没有看到任何应该使用互斥锁来锁定两个槽都可以访问的变量的警告。
也许我误解了整个“直接”和“队列”连接。
直接连接意味着发射信号和调用槽被简化为一个简单的方法调用,使得emit
调用直接跳转到槽中。 Queued connection 将调用放入队列,一旦 Qt 事件循环再次 运行 或您通过调用 QCoreApplication::processEvents()
强制执行,队列将被处理与在此之前排队的所有其他事件对齐。
在单线程应用程序中(更确切地说:当发送方和接收方对象位于同一线程中时),直接连接是默认设置,除非您在调用 QObject::connect()
时另有说明。这意味着在执行插槽或其他代码期间,此代码中的任何 emit
会立即调用连接的插槽 - 这可能是您想要的,有时不是。
你没有说你的信号有什么实际问题,但要注意永恒循环、死锁和其他 locking/mutex 问题;在单线程代码中根本不需要互斥锁。保持 signal/slot 调用链尽可能简单,并尽可能避免 emit
在槽内。
我认为您误解的不是排队版本和直接版本,而是整个 signal/slot 机制。 signal/slot 系统是回调问题的更优雅的解决方案,当您同步或异步需要知道应用程序的另一部分(可能是单线程或多线程)何时完成其工作时。
直接/排队
让我们在深入了解基础知识之前解决这个问题。
- 如果是直接连接,您的代码总是在单个线程上执行,无论插槽是否 运行 在不同的线程上。 (在大多数情况下是一个非常糟糕的主意)
- 如果连接排队并且插槽在同一线程内运行,它的行为完全就像一个直接连接。如果插槽 运行 在不同的线程上,插槽将在其 运行 所在的适当线程上下文中执行,因此使代码执行多线程。
- 如果连接是auto,这是默认的,有充分的理由,那么它会选择合适的连接类型。
现在有一种方法可以强制您的代码执行跳转到另一个线程中的槽中,即调用一个方法:
QMetaObject::invokeMethod( pointerToObject*, "functionName", Qt::QueuedConnection);
基础知识
那么让我们快速回顾一下 signal/slot 机制。系统中有三个参与者:信号、槽和连接()
信号基本上是说 "Hey! I am finished" 的消息。插槽是代码中发生 post 处理的地方,因此一旦信号完成,您就可以在代码的插槽部分执行某些操作。
connect() 是一种组织发生在何处以及出于何种原因发生的事情的方法。 signals/slots 的执行顺序与您的代码执行顺序相同。先到先得,因为你确实提到了单线程。在不同的多线程环境中。老实说,执行顺序无关紧要,如果您尝试使用 signals/slots 并需要有保证的执行顺序,那么您的应用程序设计是错误的。理想情况下,您使用 signal/slot 机制的方式与函数式编程的工作方式相同,您将消息传递给下一个实例以处理数据。
废话不多说了,让我们来看看一些实际的细节:
signals:
void a();
void b();
slots:
void sa();
void sb();
案例一
connect( a -> sa ); // Simplified Notation. Connect signal a to slot sa
connect( b -> sb );
:: emit a(); -> sa is executed
:: emit b(); -> sb is executed
案例二
connect ( a -> sa );
connect ( a -> sb );
connect ( b -> sb );
:: emit a(); -> sa & sb are executed
:: emit b(); -> sb is executed
我想这应该说清楚了。如果您还有任何问题,请告诉我
直接连接完全就像通过函数(方法)指针的调用。没有"interruption",除非你认为下面的代码中printf()
"interrupts" main()
:
// main.cpp
#include <cstdio>
int main() {
printf("Hello\n");
}
所有代码 运行 都在同一个线程中。 main()
和 printf()
永远不会 运行 并发:当 printf()
运行 时,main()
被暂停。一旦 printf()
returns,main()
恢复。 slots/functors 直接连接到信号也会发生同样的情况。
例如,让我们有以下 Object
,和一个 Monitor
class 来可视化发生了什么。
// https://github.com/KubaO/Whosebugn/tree/master/questions/sigslot-nest-38376840
#include <QtCore>
struct Monitor {
int & depth() { static int depth = 0; return depth; }
const char * const msg;
Monitor(const char * msg) : msg{msg} {
qDebug().noquote().nospace() << QString(depth()++, ' ') << msg << " entered";
}
~Monitor() {
qDebug().noquote().nospace() << QString(--depth(), ' ') << msg << " left";
}
};
struct Object : QObject {
Q_SIGNAL void signal1();
Q_SIGNAL void signal2();
Q_SLOT void slot1() { Monitor mon{__FUNCTION__}; }
Q_SLOT void slot2() { Monitor mon{__FUNCTION__}; }
Q_SLOT void slot3() {
Monitor mon{__FUNCTION__};
emit signal2();
}
Q_OBJECT
};
让我们将 signal1
直接连接到插槽 slot1
、slot2
和 slot3
。此外,让我们将 signal2
直接连接到插槽 slot1
和 slot2
:
int main() {
Monitor mon{__FUNCTION__};
Object obj;
QObject::connect(&obj, &Object::signal1, &obj, &Object::slot1);
QObject::connect(&obj, &Object::signal1, &obj, &Object::slot2);
QObject::connect(&obj, &Object::signal1, &obj, &Object::slot3);
QObject::connect(&obj, &Object::signal2, &obj, &Object::slot1);
QObject::connect(&obj, &Object::signal2, &obj, &Object::slot2);
emit obj.signal1();
}
#include "main.moc"
输出清楚地表明,当信号 运行ning 时,任何直接连接到您发出的信号的插槽都会执行 。此外,直接连接的 slots/functors 工作不需要事件循环。最后,请记住信号是 "just" 一个 moc 生成的方法,它调用所有直接连接的 slots/functors.
main entered
slot1 entered
slot1 left
slot2 entered
slot2 left
slot3 entered
slot1 entered
slot1 left
slot2 entered
slot2 left
slot3 left
main left
我很难理解什么时候会发生什么。两个不同的信号连接到两个不同的插槽,当一个插槽未完成时,将发出另一个插槽的信号(对于直接连接到各自信号的两个插槽),其中应用程序只有“一个”线程。
本文来自QT官方文档:
Direct Connection: The slot is invoked immediately, when the signal is emitted. The slot is executed in the emitter's thread, which is not necessarily the receiver's thread.
Queued Connection: 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.
与排队连接不同,直接连接说的是“立即”。这是否意味着,如果在第一个插槽尚未完成时发出第二个信号,则第一个插槽将被中断,并且即使应用程序是单线程应用程序,它也会 运行 与第二个插槽并发?如果是这样,为什么我没有看到任何应该使用互斥锁来锁定两个槽都可以访问的变量的警告。
也许我误解了整个“直接”和“队列”连接。
直接连接意味着发射信号和调用槽被简化为一个简单的方法调用,使得emit
调用直接跳转到槽中。 Queued connection 将调用放入队列,一旦 Qt 事件循环再次 运行 或您通过调用 QCoreApplication::processEvents()
强制执行,队列将被处理与在此之前排队的所有其他事件对齐。
在单线程应用程序中(更确切地说:当发送方和接收方对象位于同一线程中时),直接连接是默认设置,除非您在调用 QObject::connect()
时另有说明。这意味着在执行插槽或其他代码期间,此代码中的任何 emit
会立即调用连接的插槽 - 这可能是您想要的,有时不是。
你没有说你的信号有什么实际问题,但要注意永恒循环、死锁和其他 locking/mutex 问题;在单线程代码中根本不需要互斥锁。保持 signal/slot 调用链尽可能简单,并尽可能避免 emit
在槽内。
我认为您误解的不是排队版本和直接版本,而是整个 signal/slot 机制。 signal/slot 系统是回调问题的更优雅的解决方案,当您同步或异步需要知道应用程序的另一部分(可能是单线程或多线程)何时完成其工作时。
直接/排队
让我们在深入了解基础知识之前解决这个问题。
- 如果是直接连接,您的代码总是在单个线程上执行,无论插槽是否 运行 在不同的线程上。 (在大多数情况下是一个非常糟糕的主意)
- 如果连接排队并且插槽在同一线程内运行,它的行为完全就像一个直接连接。如果插槽 运行 在不同的线程上,插槽将在其 运行 所在的适当线程上下文中执行,因此使代码执行多线程。
- 如果连接是auto,这是默认的,有充分的理由,那么它会选择合适的连接类型。
现在有一种方法可以强制您的代码执行跳转到另一个线程中的槽中,即调用一个方法:
QMetaObject::invokeMethod( pointerToObject*, "functionName", Qt::QueuedConnection);
基础知识
那么让我们快速回顾一下 signal/slot 机制。系统中有三个参与者:信号、槽和连接()
信号基本上是说 "Hey! I am finished" 的消息。插槽是代码中发生 post 处理的地方,因此一旦信号完成,您就可以在代码的插槽部分执行某些操作。
connect() 是一种组织发生在何处以及出于何种原因发生的事情的方法。 signals/slots 的执行顺序与您的代码执行顺序相同。先到先得,因为你确实提到了单线程。在不同的多线程环境中。老实说,执行顺序无关紧要,如果您尝试使用 signals/slots 并需要有保证的执行顺序,那么您的应用程序设计是错误的。理想情况下,您使用 signal/slot 机制的方式与函数式编程的工作方式相同,您将消息传递给下一个实例以处理数据。
废话不多说了,让我们来看看一些实际的细节:
signals:
void a();
void b();
slots:
void sa();
void sb();
案例一
connect( a -> sa ); // Simplified Notation. Connect signal a to slot sa
connect( b -> sb );
:: emit a(); -> sa is executed
:: emit b(); -> sb is executed
案例二
connect ( a -> sa );
connect ( a -> sb );
connect ( b -> sb );
:: emit a(); -> sa & sb are executed
:: emit b(); -> sb is executed
我想这应该说清楚了。如果您还有任何问题,请告诉我
直接连接完全就像通过函数(方法)指针的调用。没有"interruption",除非你认为下面的代码中printf()
"interrupts" main()
:
// main.cpp
#include <cstdio>
int main() {
printf("Hello\n");
}
所有代码 运行 都在同一个线程中。 main()
和 printf()
永远不会 运行 并发:当 printf()
运行 时,main()
被暂停。一旦 printf()
returns,main()
恢复。 slots/functors 直接连接到信号也会发生同样的情况。
例如,让我们有以下 Object
,和一个 Monitor
class 来可视化发生了什么。
// https://github.com/KubaO/Whosebugn/tree/master/questions/sigslot-nest-38376840
#include <QtCore>
struct Monitor {
int & depth() { static int depth = 0; return depth; }
const char * const msg;
Monitor(const char * msg) : msg{msg} {
qDebug().noquote().nospace() << QString(depth()++, ' ') << msg << " entered";
}
~Monitor() {
qDebug().noquote().nospace() << QString(--depth(), ' ') << msg << " left";
}
};
struct Object : QObject {
Q_SIGNAL void signal1();
Q_SIGNAL void signal2();
Q_SLOT void slot1() { Monitor mon{__FUNCTION__}; }
Q_SLOT void slot2() { Monitor mon{__FUNCTION__}; }
Q_SLOT void slot3() {
Monitor mon{__FUNCTION__};
emit signal2();
}
Q_OBJECT
};
让我们将 signal1
直接连接到插槽 slot1
、slot2
和 slot3
。此外,让我们将 signal2
直接连接到插槽 slot1
和 slot2
:
int main() {
Monitor mon{__FUNCTION__};
Object obj;
QObject::connect(&obj, &Object::signal1, &obj, &Object::slot1);
QObject::connect(&obj, &Object::signal1, &obj, &Object::slot2);
QObject::connect(&obj, &Object::signal1, &obj, &Object::slot3);
QObject::connect(&obj, &Object::signal2, &obj, &Object::slot1);
QObject::connect(&obj, &Object::signal2, &obj, &Object::slot2);
emit obj.signal1();
}
#include "main.moc"
输出清楚地表明,当信号 运行ning 时,任何直接连接到您发出的信号的插槽都会执行 。此外,直接连接的 slots/functors 工作不需要事件循环。最后,请记住信号是 "just" 一个 moc 生成的方法,它调用所有直接连接的 slots/functors.
main entered
slot1 entered
slot1 left
slot2 entered
slot2 left
slot3 entered
slot1 entered
slot1 left
slot2 entered
slot2 left
slot3 left
main left