槽在哪个线程执行,是否可以重定向到另一个线程?

In which thread is a slot executed, and can I redirect it to another thread?

在进一步了解 Signal/Slot mechanic in Qt 的过程中,我对插槽执行的上下文感到困惑,因此我编写了以下示例来对其进行测试:

from PyQt5.Qt import * # I know this is bad, but I want a small example
import threading

def slot_to_output_something ( something ):
    print( 'slot called by', threading.get_ident(), 'with', something )

class Object_With_A_Signal( QObject ):
    sig = pyqtSignal( str )

class LoopThread( QThread ):
    def __init__ ( self, object_with_a_signal ):
        self.object_with_a_signal = object_with_a_signal
        super().__init__()

    def run ( self ):
        print( 'loop running in', threading.get_ident() )
        import time
        for i in range( 5 ):
            self.object_with_a_signal.sig.emit( str( i ) )
            time.sleep( 1 )


print( 'main running in', threading.get_ident() )
app = QApplication( [] )
mainw = QMainWindow( None )
mainw.show()
obj = Object_With_A_Signal()

# connection in main-thread
obj.sig.connect(slot_to_output_something, Qt.QueuedConnection )
loop = LoopThread( obj )
loop.start()

app.exec()

输出:

main running in 57474
loop running in 57528
slot called by 57474 with 0
slot called by 57474 with 1
...


到目前为止还不错 - 但现在我找到了 他说的地方:

Your slot will always be executed in the calling thread, except you create a Qt::QueuedConnection to run the slot in the thread the object owning the slot belongs to.

插槽的所有权在 Python 中如何工作? 据我的下一次尝试显示,插槽连接到信号的线程是线程执行插槽,当信号被发出时:

# connection in main-thread
# obj.sig.connect(slot_to_output_something, Qt.QueuedConnection )
# loop = LoopThread( obj )
# loop.start()

# connection in helper-thread
class Thread_In_Between( QThread ):
    def __init__ ( self, object_with_a_signal ):
        super().__init__()
        self.object_with_a_signal = object_with_a_signal

    def run ( self ):
        print( 'helper thread running in', threading.get_ident() )
        self.object_with_a_signal.sig.connect( slot_to_output_something, Qt.QueuedConnection)
        loop = LoopThread( self.object_with_a_signal )
        loop.start()
        loop.exec()  # without -> ERROR: QThread: Destroyed while thread is still running
        print( 'end helper thread' ) # never reached ??

helper_thread = Thread_In_Between( obj )
helper_thread.start()

输出:

main running in 65804
helper thread running in 65896
loop running in 65900
slot called by 65896 with 0
slot called by 65896 with 1
...

所以..我说得对吗? 槽是由线程执行的,它们在其中连接,还是我只是想出了一个错误的例子?


此外,GUI 更改应该只在主线程中执行,但是如果我将这些行添加到我的代码中

# use QListwidget for output instead
lis = QListWidget( None )
print = lambda *args: lis.addItem( str( ' '.join( str( x ) for x in args ) ) )
mainw.setCentralWidget( lis )

输出被重定向到 QListWidget,但表明这不是在主线程中调用的。是否有将插槽移动到另一个线程的选项(传输 "ownership" - 我刚刚找到 QObject::moveToThread)?


它们是关于使用 pyqt 执行调用槽(通过发出信号)的一般规则吗?

编辑:
整个问题只是关于 QueuedConnectionBlockingQueuedConnection。我知道 DirectConnectionother options.

PyQt 中有两种主要类型的槽:一种是包装的 Qt 槽。和普通的 python 可调用对象。

第一种类型包括 Qt 定义的内置槽,以及用 pyqtSlot. These slots will work exactly as documented by Qt, so there are no additional PyQt-specific "rules" that apply to them. By definition, they must be members of a QObject subclass, which in turn means they are part of the Meta Object System. You can therefore explictly check that a slot is of this type by using, e.g. indexOfSlot 装饰的任何用户定义槽。

对于第二种槽,PyQt 创建了一个内部代理对象,它包装了 python 可调用对象,并提供了信号槽机制所需的 Qt 槽。因此,这就提出了这个代理对象应该存在于何处的问题。如果可调用对象属于继承 QObject 的对象,PyQt 可以自动将代理移动到适当的线程。在伪代码中,它会做这样的事情:

if receiver:
    proxy.moveToThread(receiver.thread())

但是,如果没有合适的接收者,代理将停留在创建它的任何线程中。

后一种情况适用于您的示例。 slot_to_output_something 插槽只是一个没有所有者的模块级函数。 PyQt 找不到与其相关联的接收器,因此内部代理将停留在建立连接的线程中。然而,如果这个插槽被移动成为 Object_With_A_Signal 的成员,它将在 主线程 中被调用。这是因为 Object_With_A_Signal 继承了 QObject 并且它的实例当前位于主线程中。这允许 PyQt 自动将内部代理移动到适当接收器的线程。

因此,如果您想控制插槽的执行位置,请将其设为 QObject 子类的成员,如有必要,请使用 moveToThread to explicitly place it in the appropriate thread. In addition, it is probably advisable to apply the pyqtSlot decorator so as to avoid any awkward corner cases (see this answer 了解详细信息)。

PS:

上述 "rules" 第二种类型的槽可能只适用于 PyQt - PySide 不太可能以同样的方式工作。而且也可能无法保证它们将以与所有以前或未来版本的 PyQt 完全相同的方式工作。因此,如果您想避免意外的行为变化,最好将 pyqtSlot 装饰器与将在不同线程之间连接的任何插槽一起使用。