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

时间:2018-08-23 16:02:51

标签: python multithreading pyqt signals-slots qthread

在深入了解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有关。我知道有DirectConnectionother options

1 个答案:

答案 0 :(得分:2)

PyQt中有两种主要的插槽类型:包裹Qt插槽的插槽。还有那些是普通的python可调用对象。

第一种类型包括Qt定义的内置插槽,以及任何用pyqtSlot装饰的用户定义的插槽。这些插槽将完全按照Qt的说明工作,因此没有适用于它们的其他PyQt特定的“规则”。根据定义,它们必须是QObject子类的成员,这又意味着它们是Meta Object System的一部分。因此,您可以使用以下方式明确检查某个插槽是否属于此类: 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将其显式放置在适当的线程中。另外,建议使用pyqtSlot装饰器,以避免出现任何尴尬的转折情况(有关详细信息,请参见this answer)。

PS

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