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

佐手、 提交于 2021-02-08 10:21:22

问题


I have a Worker object and use its method moveToThread to put it in a thread.

Now i call its work method:

  • If I invoke the method directly, it is executed in the thread its object is living in
  • If I invoke the method using lambda, the method is executed in the main thread

Example:

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()

I have done excessive testing (see code in snippet) but i have absolutly no idea what's going on.

So my question is:

Why is work executed in the main thread and not the thread its object lives in if it is invoked with lambda? And most important what can i do if i want to call a method of Worker requiring arguments where i cant spare the 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_())

回答1:


I think I have an answer.

I tried you code and and changed the connect method for clicked to use a QtCore.Qt.QueuedConnection and QtCore.Qt.DirectConnection (connect(self.worker.work, Qt.QueuedConnection)). The Direct connection makes both functions work the same way (the way that lambda works) they both run in the main thread. However, the Queued connection makes them work differently. The lambda function still runs in the main thread while the worker function call runs in the separate thread. Note: without giving an argument in connect you are using AutoConnection which will use the QueuedConnection.

I read through the documentation 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.

So I believe that lambda is running in the main thread, because lambda is creating a new function. The new lambda function is not a slot and exists in the main thread. The lambda functions receiver's thread is the main thread. Meanwhile the worker.work method is a slot that has a different receiver thread. So the signal knows to call self.worker.work in the worker thread while it calls the lambda function in the main thread which then calls self.worker.work() in the main thread.

I know this is inconvenient, because lambda is useful for passing arguments to a function.

Use Signal Mapper to Pass Values

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()


来源:https://stackoverflow.com/questions/43937897/pyside-method-is-not-executed-in-thread-context-if-method-is-invoked-via-lambda

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!