PySide:如果通过 lambda 调用方法,则方法不会在线程上下文中执行

PySide: method is not executed in thread context if method is invoked via lambda

我有一个 Worker 对象并使用它的方法 moveToThread 将它放在一个线程中。

现在我调用它的work方法:

示例:

from PySide.QtCore import *
from PySide.QtGui import *
import sys

class Worker(QObject):
    def __init__(self):
        super().__init__()

    def work(self):
        print(self.thread().currentThread())


class Example(QWidget):
    def __init__(self):
        super().__init__()
        self.btnInThread = QPushButton('in thread')
        self.btnNotInThread = QPushButton('not in thread')
        layout = QVBoxLayout()
        layout.addWidget(self.btnInThread)
        layout.addWidget(self.btnNotInThread)
        self.setLayout(layout)

        self.worker = Worker()
        self.Thread = QThread()
        self.worker.moveToThread(self.Thread)
        self.Thread.start()

        self.btnInThread.clicked.connect(self.worker.work)
        self.btnNotInThread.clicked.connect(lambda: self.worker.work())

        self.show()
        print('{0} <- Main Thread'.format(self.thread().currentThread()))


def main():
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

我做了过多的测试(参见片段中的代码),但我完全不知道发生了什么。

所以我的问题是:

为什么work在主线程中执行,而不是在lambda调用时在其对象所在的线程中执行?最重要的是,如果我想调用 Worker 的方法需要参数,而我无法节省 lambda?

,我该怎么办

from PySide.QtCore import *
from PySide.QtGui import *
from time import sleep
import functools
import sys


class Worker(QObject):
    def __init__(self):
        super().__init__()

    def work(self, name='Nothing'):
        print('Thread ID: {1} - {0} start'.format(name, QThread.currentThreadId()))
        sleep(1)
        print('##### End {0}'.format(name))


class HackPushButton(QPushButton):
    clicked_with_arg = Signal(str)
    def __init__(self, *args):
        super().__init__(*args)
        self.argument = None
        self.clicked.connect(lambda: self.clicked_with_arg.emit(self.argument))


class Example(QWidget):
    def __init__(self):
        super().__init__()
        self.buttonWithoutLambda = QPushButton('[Works] Call work() without arguments and without lambda')
        self.buttonWithLambda = QPushButton('[Blocks] Call work() with arguments and with lambda')
        self.buttonWithFunctools = QPushButton('[Blocks] Call work() with arguments and with functools')
        self.buttonWithHelperFunctionWithArgument = QPushButton('[Blocks] Call work() with arguments and with helper function')
        self.buttonWithHelperFunctionWithoutArgument = QPushButton('[Blocks] Call work() without arguments and with helper function')
        self.buttonWithHack = HackPushButton('[Works] Call work() with arguments via dirty hack')
        layout = QVBoxLayout()
        layout.addWidget(self.buttonWithoutLambda)
        layout.addWidget(self.buttonWithLambda)
        layout.addWidget(self.buttonWithFunctools)
        layout.addWidget(self.buttonWithHelperFunctionWithArgument)
        layout.addWidget(self.buttonWithHelperFunctionWithoutArgument)
        layout.addWidget(self.buttonWithHack)
        self.setLayout(layout)

        self.Worker = Worker()
        self.Thread = QThread()
        self.Worker.moveToThread(self.Thread)
        self.Thread.start()

        # Doesn't block GUI
        self.buttonWithoutLambda.clicked.connect(self.Worker.work)

        # Blocks GUI
        self.buttonWithLambda.clicked.connect(lambda: self.Worker.work('Lambda'))

        # Blocks GUI
        self.buttonWithFunctools.clicked.connect(functools.partial(self.Worker.work, 'Functools'))

        # Blocks GUI
        self.helperFunctionArgument = 'Helper function without arguments'
        self.buttonWithHelperFunctionWithArgument.clicked.connect(self.helperFunctionWithArgument)

        # Blocks GUI
        self.buttonWithHelperFunctionWithoutArgument.clicked.connect(self.helperFunctionWithoutArgument)

        # Doesn't block GUI
        self.buttonWithHack.argument = 'Hack'
        self.buttonWithHack.clicked_with_arg.connect(self.Worker.work)

        print('Thread ID: {0}'.format(QThread.currentThreadId()))
        self.show()

    def helperFunctionWithArgument(self):
        self.Worker.work(self.helperFunctionArgument)

    def helperFunctionWithoutArgument(self):
        self.Worker.work()


app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())

我想我有答案了。

我尝试了您的代码,并更改了 connect 方法以使用 QtCore.Qt.QueuedConnectionQtCore.Qt.DirectConnection (connect(self.worker.work, Qt.QueuedConnection))。直接连接使两个函数以相同的方式(lambda 的工作方式)在主线程中都 运行 工作。但是,队列连接使它们的工作方式不同。 lambda 函数仍在主线程中 运行s,而 worker 函数在单独的线程中调用 运行s。注意:如果不在 connect 中给出参数,您将使用 AutoConnection,它将使用 QueuedConnection。

我通读了文档 http://doc.qt.io/qt-4.8/threads-qobject.html#signals-and-slots-across-threads

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.

所以我相信 lambda 运行ning 在主线程中,因为 lambda 正在创建一个新函数。新的 lambda 函数不是槽,存在于主线程中。 lambda 函数接收者的线程是主线程。同时,worker.work 方法是一个具有不同接收线程的插槽。因此,信号知道在工作线程中调用 self.worker.work,同时在主线程中调用 lambda 函数,然后在主线程中调用 self.worker.work()

我知道这很不方便,因为 lambda 对于将参数传递给函数很有用。

使用信号映射器传递值

from PySide import QtCore
from PySide import QtGui
import sys
import time


def create_map(obj, func, args=None):
    """Create a signal mapper to associate a value with a function.

    Args:
        obj (QObject): Object to map the value to with the signal mapper
        func (function): Function to run when the signal mapper.map function is called.
        args (tuple)[None]: Arguments you want to pass to the function.

    Returns:
        map_callback (function): Map function to connect to a signal.
        mapper (QSignalMapper): You may need to keep a reference of the signal mapper object.
    """
    mapper = QtCore.QSignalMapper()
    mapper.setMapping(obj, args)
    mapper.mapped.connect(func)
    return mapper.map, mapper


class Worker(QtCore.QObject):

    def __init__(self):
        super().__init__()

    def work(self, value=0):
        print(self.thread().currentThread())
        time.sleep(2)
        print("end", value)


class Example(QtGui.QWidget):
    def __init__(self):
        super().__init__()
        self.btnInThread = QtGui.QPushButton('in thread')
        self.btnNotInThread = QtGui.QPushButton('not in thread')
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self.btnInThread)
        layout.addWidget(self.btnNotInThread)
        self.setLayout(layout)

        self.worker = Worker()
        self.Thread = QtCore.QThread()
        self.worker.moveToThread(self.Thread)
        self.Thread.start()

        self.btnInThread.clicked.connect(self.worker.work)

        # Use a signal mapper
        # self.mapper = QtCore.QSignalMapper()
        # self.mapper.setMapping(self.btnNotInThread, 1)
        # self.mapper.mapped.connect(self.worker.work)
        # self.btnNotInThread.clicked.connect(self.mapper.map)

        # Alternative mapper method from above
        callback, self.mapper = create_map(self.btnNotInThread, self.worker.work, 1)
        self.btnNotInThread.clicked.connect(callback)

        self.show()
        print('{0} <- Main Thread'.format(self.thread().currentThread()))


def main():
    app = QtGui.QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()