PySide:如果通过lambda调用方法,则方法不会在线程上下文中执行 [英] PySide: method is not executed in thread context if method is invoked via lambda

查看:88
本文介绍了PySide:如果通过lambda调用方法,则方法不会在线程上下文中执行的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

现在我调用它的work方法:

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

示例:

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)). Direct连接使这两个函数在主线程中运行的方式相同(lambda的工作方式).但是,排队连接使它们的工作方式有所不同. lambda函数仍在主线程中运行,而worker函数调用在单独的线程中运行.注意:在connect中没有给出参数,您正在使用自动连接,它将使用QueuedConnection.

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

排队的连接当控制返回到接收者线程的事件循环时,将调用该插槽.该插槽在接收者的线程中执行.

因此,我相信lambda在主线程中运行,因为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()

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

解决方案

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

这篇关于PySide:如果通过lambda调用方法,则方法不会在线程上下文中执行的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆