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

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

我有一个Worker对象,并使用其方法moveToThread将其放入线程中。

现在我叫它的work方法:

  • 直接调用该方法,该方法在其对象所在的线程中执行。
  • 如果我使用lambda调用该方法,则该方法在主线程中执行

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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()

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

所以我的问题是:

为什么是在主线程中执行的,而不是如果用lambda调用它的对象所在的线程?最重要的是,如果我想调用Worker的方法并需要参数而无法保留lambda的话,该怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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))。 Direct连接使这两个函数在主线程中运行的方式相同(lambda的工作方式)。但是,排队连接使它们的工作方式有所不同。 lambda函数仍在主线程中运行,而worker函数调用在单独的线程中运行。注意:没有在connect中提供参数,您正在使用自动连接,它将使用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在主线程中运行,因为lambda正在创建一个新函数。新的lambda函数不是插槽,而是存在于主线程中。 lambda函数接收者的线程是主线程。同时,worker.work方法是具有不同接收器线程的插槽。因此,信号知道在工作线程中调用self.worker.work,同时又在主线程中调用了lambda函数,然后又在主线程中调用了self.worker.work()

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

使用信号映射器传递值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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()