问题
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