插槽在哪个线程中执行,我可以将其重定向到另一个线程吗?

Posted

技术标签:

【中文标题】插槽在哪个线程中执行,我可以将其重定向到另一个线程吗?【英文标题】:In which thread is a slot executed, and can I redirect it to another thread? 【发布时间】:2019-01-30 01:54:10 【问题描述】:

在学习更多关于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()

输出:

主要运行在 57474 循环在 57528 中运行 由 57474 以 0 调用的插槽 插槽由 57474 调用,带有 1 ...


到目前为止还不错 - 但现在我找到了answer of Sebastian Lange,他说:

您的插槽将始终在调用线程中执行,除非您创建一个Qt::QueuedConnection 来在拥有该插槽的对象所属的线程中运行该插槽。

槽的所有权在 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()

输出:

主要运行在 65804 在 65896 中运行的辅助线程 循环运行在 65900 插槽由 65896 调用,带有 0 插槽由 65896 调用,带有 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,但表明它没有在主线程中调用。是否可以选择将插槽移动到另一个线程(转移“所有权” - 我刚刚找到 QObject::moveToThread)?


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

编辑: 这整个问题只是关于QueuedConnectionBlockingQueuedConnection。我知道DirectConnection 和other options。

【问题讨论】:

为什么要在另一个线程中创建线程?循环被破坏,因为它是一个局部变量 否 - 使用QueuedConnection,槽在接收者的线程中执行。 @eyllanesc:我创建了另一个线程来查看,如果他是执行该插槽的人,情况确实如此。此外,loop 不会被破坏,因为代码不会越过loop.exec() 行,它不会离开事件循环(不知道为什么?)。 @ekhumoro: 并且是他们的一套规则,决定了谁是接收线程? 【参考方案1】:

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

第一种类型包括 Qt 定义的内置槽,以及任何用pyqtSlot 修饰的用户定义槽。这些插槽将完全按照 Qt 记录的方式工作,因此没有其他适用于它们的 PyQt 特定“规则”。根据定义,它们必须是QObject 子类的成员,这反过来意味着它们是Meta Object System 的一部分。因此,您可以通过使用例如明确地检查插槽是否属于这种类型。 indexOfSlot.

对于第二种类型的槽,PyQt 创建了一个内部代理对象,该对象包装了 python 可调用对象,并提供了信号槽机制所需的 Qt 槽。因此,这就提出了这个代理对象应该在哪里生活的问题。如果 callable 由继承 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 将其显式放置在适当的线程中。此外,建议使用 pyqtSlot 装饰器以避免任何尴尬的极端情况(详见this answer)。

PS

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

【讨论】:

以上是关于插槽在哪个线程中执行,我可以将其重定向到另一个线程吗?的主要内容,如果未能解决你的问题,请参考以下文章

Pharo Smalltalk - 消息转发,是不是可以截获消息并将其重定向到另一个对象(实例)?

在函数中抑制 cout 输出

在用户填写提交表单后将其重定向到他们的提交

BOOST 如何在一个线程中发送信号并在另一个线程中执行相应的插槽?

从进度输出中删除进度条,将其重定向到日志文件中

QT - >如果线程正在消耗所有资源并且不允许插槽执行,如何杀死线程