我正在尝试编写一个响应的短(一个文件pyqt)程序(因此python / lxml / qt之外的依赖关系,特别是我不能只是坚持在文件中的这个用例有一些缺点但我可能还是愿意尝试一下)。 我正在尝试在工作线程上执行可能冗长(和可取消)的操作(实际上后台操作有一个锁定它以防止一次多个操作(因为它使用的库一次只能使用一个调用)和产生多个线程的超时也没问题。
据我所知,使用qt这样做的“基本”方法是。 (注意代码未经过测试,因此可能出错)
class MainWindow(QWidget):
#self.worker moved to background thread
def initUI(self):
...
self.cmd_button.clicked.connect(self.send)
...
@pyqtslot()
def send(self):
...
...#get cmd from gui
QtCore.QTimer.singleShot(0, lambda : self.worker(cmd))
@pyqtslot(str)
def end_send(self, result):
...
...# set some gui to display result
...
class WorkerObject(QObject):
def send_cmd(self, cmd):
... get result of cmd
QtCore.QTimer.singleShot(0, lambda: self.main_window.end_send())
(我是否正确使用QTimer(它在不同的线程上运行)?)
我真的更喜欢按照c#异步的方式更简单,更抽象。 (注意我没有使用asyncio所以我可能会遇到一些问题)
class MainWindow(QWidget):
...
@asyncio.coroutine
def send(self):
...
...#get cmd from gui
result = yield from self.worker(cmd)
#set gui textbox to result
class WorkerObject(QObject):
@asyncio.coroutine
def send_cmd(self, cmd):
... get result of cmd
yield from loop.run_in_executor(None, self.model.send_command, cmd)
我听说python 3有类似的功能,并且有一个后端口,但它是否与qt一起正常工作?
如果有人知道另一种理智模式。这也是有用的/可接受的答案。
答案 0 :(得分:15)
对你的问题的简短回答(“有没有办法在PyQt中使用类似asyncio
的模式?”)是肯定的,但它很复杂,对于一个小程序来说可能是不值得的。这里有一些原型代码,允许您使用如您所描述的异步模式:
import types
import weakref
from functools import partial
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import QThread, QTimer
## The following code is borrowed from here:
# http://stackoverflow.com/questions/24689800/async-like-pattern-in-pyqt-or-cleaner-background-call-pattern
# It provides a child->parent thread-communication mechanism.
class ref(object):
"""
A weak method implementation
"""
def __init__(self, method):
try:
if method.im_self is not None:
# bound method
self._obj = weakref.ref(method.im_self)
else:
# unbound method
self._obj = None
self._func = method.im_func
self._class = method.im_class
except AttributeError:
# not a method
self._obj = None
self._func = method
self._class = None
def __call__(self):
"""
Return a new bound-method like the original, or the
original function if refers just to a function or unbound
method.
Returns None if the original object doesn't exist
"""
if self.is_dead():
return None
if self._obj is not None:
# we have an instance: return a bound method
return types.MethodType(self._func, self._obj(), self._class)
else:
# we don't have an instance: return just the function
return self._func
def is_dead(self):
"""
Returns True if the referenced callable was a bound method and
the instance no longer exists. Otherwise, return False.
"""
return self._obj is not None and self._obj() is None
def __eq__(self, other):
try:
return type(self) is type(other) and self() == other()
except:
return False
def __ne__(self, other):
return not self == other
class proxy(ref):
"""
Exactly like ref, but calling it will cause the referent method to
be called with the same arguments. If the referent's object no longer lives,
ReferenceError is raised.
If quiet is True, then a ReferenceError is not raise and the callback
silently fails if it is no longer valid.
"""
def __init__(self, method, quiet=False):
super(proxy, self).__init__(method)
self._quiet = quiet
def __call__(self, *args, **kwargs):
func = ref.__call__(self)
if func is None:
if self._quiet:
return
else:
raise ReferenceError('object is dead')
else:
return func(*args, **kwargs)
def __eq__(self, other):
try:
func1 = ref.__call__(self)
func2 = ref.__call__(other)
return type(self) == type(other) and func1 == func2
except:
return False
class CallbackEvent(QtCore.QEvent):
"""
A custom QEvent that contains a callback reference
Also provides class methods for conveniently executing
arbitrary callback, to be dispatched to the event loop.
"""
EVENT_TYPE = QtCore.QEvent.Type(QtCore.QEvent.registerEventType())
def __init__(self, func, *args, **kwargs):
super(CallbackEvent, self).__init__(self.EVENT_TYPE)
self.func = func
self.args = args
self.kwargs = kwargs
def callback(self):
"""
Convenience method to run the callable.
Equivalent to:
self.func(*self.args, **self.kwargs)
"""
self.func(*self.args, **self.kwargs)
@classmethod
def post_to(cls, receiver, func, *args, **kwargs):
"""
Post a callable to be delivered to a specific
receiver as a CallbackEvent.
It is the responsibility of this receiver to
handle the event and choose to call the callback.
"""
# We can create a weak proxy reference to the
# callback so that if the object associated with
# a bound method is deleted, it won't call a dead method
if not isinstance(func, proxy):
reference = proxy(func, quiet=True)
else:
reference = func
event = cls(reference, *args, **kwargs)
# post the event to the given receiver
QtGui.QApplication.postEvent(receiver, event)
## End borrowed code
## Begin Coroutine-framework code
class AsyncTask(QtCore.QObject):
""" Object used to manage asynchronous tasks.
This object should wrap any function that you want
to call asynchronously. It will launch the function
in a new thread, and register a listener so that
`on_finished` is called when the thread is complete.
"""
def __init__(self, func, *args, **kwargs):
super(AsyncTask, self).__init__()
self.result = None # Used for the result of the thread.
self.func = func
self.args = args
self.kwargs = kwargs
self.finished = False
self.finished_cb_ran = False
self.finished_callback = None
self.objThread = RunThreadCallback(self, self.func, self.on_finished,
*self.args, **self.kwargs)
self.objThread.start()
def customEvent(self, event):
event.callback()
def on_finished(self, result):
""" Called when the threaded operation is complete.
Saves the result of the thread, and
executes finished_callback with the result if one
exists. Also closes/cleans up the thread.
"""
self.finished = True
self.result = result
if self.finished_callback:
self.finished_ran = True
func = partial(self.finished_callback, result)
QTimer.singleShot(0, func)
self.objThread.quit()
self.objThread.wait()
class RunThreadCallback(QtCore.QThread):
""" Runs a function in a thread, and alerts the parent when done.
Uses a custom QEvent to alert the main thread of completion.
"""
def __init__(self, parent, func, on_finish, *args, **kwargs):
super(RunThreadCallback, self).__init__(parent)
self.on_finished = on_finish
self.func = func
self.args = args
self.kwargs = kwargs
def run(self):
try:
result = self.func(*self.args, **self.kwargs)
except Exception as e:
print "e is %s" % e
result = e
finally:
CallbackEvent.post_to(self.parent(), self.on_finished, result)
def coroutine(func):
""" Coroutine decorator, meant for use with AsyncTask.
This decorator must be used on any function that uses
the `yield AsyncTask(...)` pattern. It shouldn't be used
in any other case.
The decorator will yield AsyncTask objects from the
decorated generator function, and register itself to
be called when the task is complete. It will also
excplicitly call itself if the task is already
complete when it yields it.
"""
def wrapper(*args, **kwargs):
def execute(gen, input=None):
if isinstance(gen, types.GeneratorType):
if not input:
obj = next(gen)
else:
try:
obj = gen.send(input)
except StopIteration as e:
result = getattr(e, "value", None)
return result
if isinstance(obj, AsyncTask):
# Tell the thread to call `execute` when its done
# using the current generator object.
func = partial(execute, gen)
obj.finished_callback = func
if obj.finished and not obj.finished_cb_ran:
obj.on_finished(obj.result)
else:
raise Exception("Using yield is only supported with AsyncTasks.")
else:
print("result is %s" % result)
return result
result = func(*args, **kwargs)
execute(result)
return wrapper
## End coroutine-framework code
如果您将上述代码放入模块(例如qtasync.py
),您可以将其导入到脚本中并像这样使用它来获取asyncio
- 就像行为一样:
import sys
import time
from qtasync import AsyncTask, coroutine
from PyQt4 import QtGui
from PyQt4.QtCore import QThread
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
self.cmd_button = QtGui.QPushButton("Push", self)
self.cmd_button.clicked.connect(self.send_evt)
self.statusBar()
self.show()
def worker(self, inval):
print "in worker, received '%s'" % inval
time.sleep(2)
return "%s worked" % inval
@coroutine
def send_evt(self, arg):
out = AsyncTask(self.worker, "test string")
out2 = AsyncTask(self.worker, "another test string")
QThread.sleep(3)
print("kicked off async task, waiting for it to be done")
val = yield out
val2 = yield out2
print ("out is %s" % val)
print ("out2 is %s" % val2)
out = yield AsyncTask(self.worker, "Some other string")
print ("out is %s" % out)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
m = MainWindow()
sys.exit(app.exec_())
输出(按下按钮时):
in worker, received 'test string'
in worker, received 'another test string'
kicked off async task, waiting for it to be done
out is test string worked
out2 is another test string worked
in worker, received 'Some other string'
out is Some other string worked
正如您所看到的,worker
只要通过AsyncTask
类调用它就会在线程中异步运行,但它的返回值可以yield
直接来自send_evt
无需使用回调。
代码使用Python生成器的协程支持功能(generator_object.send
),以及提供child->主线程通信机制的recipe I found on ActiveState来实现一些非常基本的协同程序。协同程序非常有限:你无法从它们返回任何内容,也无法将协程调用链接在一起。除非你确实需要它们,否则可能实现这两个方面,但也可能不值得付出努力。我也没有对此做过多次负面测试,因此工作人员和其他地方的例外情况可能无法正常处理。但它做什么做得好,允许你通过AsyncTask
类在单独的线程中调用方法,然后yield
一个准备就绪后来自线程的结果, 不阻止Qt事件循环。通常情况下,这样的事情将通过回调来完成,回调很难遵循,并且通常比在单个函数中包含所有代码的可读性低。
如果我提到的限制是可以接受的,欢迎您使用这种方法,但这只是一个概念验证;在考虑将其投入生产之前,您需要进行一大堆测试。
正如您所提到的,Python 3.3和3.4分别通过引入yield from
和asyncio
使异步编程更容易。我认为yield from
在这里实际上非常有用,允许链接协同程序(意味着有一个协程调用另一个协程并从中获取结果)。 asyncio
没有PyQt4事件循环集成,所以它的用处非常有限。
另一种选择是完全删除协同程序的一部分,直接使用callback-based inter-thread communication mechanism:
import sys
import time
from qtasync import CallbackEvent # No need for the coroutine stuff
from PyQt4 import QtGui
from PyQt4.QtCore import QThread
class MyThread(QThread):
""" Runs a function in a thread, and alerts the parent when done.
Uses a custom QEvent to alert the main thread of completion.
"""
def __init__(self, parent, func, on_finish, *args, **kwargs):
super(MyThread, self).__init__(parent)
self.on_finished = on_finish
self.func = func
self.args = args
self.kwargs = kwargs
self.start()
def run(self):
try:
result = self.func(*self.args, **self.kwargs)
except Exception as e:
print "e is %s" % e
result = e
finally:
CallbackEvent.post_to(self.parent(), self.on_finished, result)
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
self.cmd_button = QtGui.QPushButton("Push", self)
self.cmd_button.clicked.connect(self.send)
self.statusBar()
self.show()
def customEvent(self, event):
event.callback()
def worker(self, inval):
print("in worker, received '%s'" % inval)
time.sleep(2)
return "%s worked" % inval
def end_send(self, cmd):
print("send returned '%s'" % cmd)
def send(self, arg):
t = MyThread(self, self.worker, self.end_send, "some val")
print("Kicked off thread")
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
m = MainWindow()
sys.exit(app.exec_())
输出:
Kicked off thread
in worker, received 'some val'
send returned 'some val worked'
如果你正在处理一个很长的回调链,这可能会有点笨拙,但它不依赖于更加未经验证的coroutine
代码。
答案 1 :(得分:2)
如果你想要一个简单的(根据所需的代码行)方法,你可以创建一个QThread并使用pyqtSignal
在线程完成时提醒父节点。这里有两个按钮。一个控制可以取消的后台线程。第一次推送将线程关闭,第二次推送取消后台线程。在后台线程运行时,另一个按钮将自动禁用,并在完成后重新启用。
from PyQt4 import QtGui
from PyQt4 import QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
self.task = None
def initUI(self):
self.cmd_button = QtGui.QPushButton("Push/Cancel", self)
self.cmd_button2 = QtGui.QPushButton("Push", self)
self.cmd_button.clicked.connect(self.send_cancellable_evt)
self.cmd_button2.clicked.connect(self.send_evt)
self.statusBar()
self.layout = QtGui.QGridLayout()
self.layout.addWidget(self.cmd_button, 0, 0)
self.layout.addWidget(self.cmd_button2, 0, 1)
widget = QtGui.QWidget()
widget.setLayout(self.layout)
self.setCentralWidget(widget)
self.show()
def send_evt(self, arg):
self.t1 = RunThread(self.worker, self.on_send_finished, "test")
self.t2 = RunThread(self.worker, self.on_send_finished, 55)
print("kicked off async tasks, waiting for it to be done")
def worker(self, inval):
print "in worker, received '%s'" % inval
time.sleep(2)
return inval
def send_cancellable_evt(self, arg):
if not self.task:
self.task = RunCancellableThread(None, self.on_csend_finished, "test")
print("kicked off async task, waiting for it to be done")
else:
self.task.cancel()
print("Cancelled async task.")
def on_csend_finished(self, result):
self.task = None # Allow the worker to be restarted.
print "got %s" % result
def on_send_finished(self, result):
print "got %s. Type is %s" % (result, type(result))
class RunThread(QtCore.QThread):
""" Runs a function in a thread, and alerts the parent when done.
Uses a pyqtSignal to alert the main thread of completion.
"""
finished = QtCore.pyqtSignal(["QString"], [int])
def __init__(self, func, on_finish, *args, **kwargs):
super(RunThread, self).__init__()
self.args = args
self.kwargs = kwargs
self.func = func
self.finished.connect(on_finish)
self.finished[int].connect(on_finish)
self.start()
def run(self):
try:
result = self.func(*self.args, **self.kwargs)
except Exception as e:
print "e is %s" % e
result = e
finally:
if isinstance(result, int):
self.finished[int].emit(result)
else:
self.finished.emit(str(result)) # Force it to be a string by default.
class RunCancellableThread(RunThread):
def __init__(self, *args, **kwargs):
self.cancelled = False
super(RunCancellableThread, self).__init__(*args, **kwargs)
def cancel(self):
self.cancelled = True # Use this if you just want to signal your run() function.
# Use this to ungracefully stop the thread. This isn't recommended,
# especially if you're doing any kind of work in the thread that could
# leave things in an inconsistent or corrupted state if suddenly
# terminated
#self.terminate()
def run(self):
try:
start = cur_time = time.time()
while cur_time - start < 10:
if self.cancelled:
print("cancelled")
result = "cancelled"
break
print "doing work in worker..."
time.sleep(1)
cur_time = time.time()
except Exception as e:
print "e is %s" % e
result = e
finally:
if isinstance(result, int):
self.finished[int].emit(result)
else:
self.finished.emit(str(result)) # Force it to be a string by default.
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
m = MainWindow()
sys.exit(app.exec_())
输出(推“推”):
in worker, received 'test'kicked off async tasks, waiting for it to be done
in worker, received '55'
got 55. Type is <type 'int'>
got test. Type is <class 'PyQt4.QtCore.QString'>
in worker, received 'test'
in worker, received '55'
输出(按“推/取”):
kicked off async task, waiting for it to be done
doing work in worker...
doing work in worker...
doing work in worker...
doing work in worker...
doing work in worker...
doing work in worker...
<I pushed the button again here>
Cancelled async task.
cancelled
got cancelled
这里有一些恼人的限制:
finished
信号不易处理任意类型。您必须为要返回的每个类型显式声明和连接处理程序,然后在获得结果时确保emit
到正确的处理程序。这称为信号过载。一些具有相同C ++签名won't behave right when used together like this的Python类型,例如pyqtSignal([dict], [list])
。最终您可能更容易创建几个不同的QThread
子类来处理您可以从线程中运行的东西返回的不同类型。RunThread
对象的引用,否则当它超出范围时会立即被销毁,该范围在完成之前终止在线程中运行的工作程序。这有点浪费,因为你在完成它之后会保留对已完成的QThread
对象的引用(除非你在on_finish
处理程序或其他机制中清理它)。 / LI>
醇>
答案 2 :(得分:0)
对于运行一个冗长的处理,创建依赖于QThreads的事件驱动的工作对象是复杂的。一次调用单个处理方法有两种方法:
第一种方法是QtConcurrent()
。如果你只是运行冗长的功能,这将是一个很好的方法。不知道这是否在pyqt中可用。
第二种方法是子类化QThread并在子类的run()
方法中实现处理代码。然后只需拨打QThreadSubclass.start()
。这应该在PyQt中可用,并且可能是要走的路。复杂性缩小到一个简单的子类。与线程的通信很容易实现,因为您可以与任何其他类进行通信。
当使用分配给QThread的对象时,这可能不是最好的方法,而不是QTimer,你应该使用Qt.QueuedConnection发出信号。使用QueuedConnection将确保对象所在的插槽正在运行。
答案 3 :(得分:0)
这是我目前正在考虑的解决方案
它受到了c#&#39>的模糊启发
task.ContinueWith(() => { /*some code*/ }, TaskScheduler.FromCurrentSynchronizationContext());
并且有点适合我的一个主线程/一个工作线程情况,虽然它可以通过创建新线程轻松推广(最终你可能想使用某种线程池我认为qt有一个但我没有&t; t尝试过。)
基本上,您安排函数在特定线程上运行,并使用带有嵌套run_on_thread调用的闭包来更改ui。为了简化事情,我没有添加返回值(只需将东西分配给对象/使用本地vair上的闭包。
我还没有真正想过如何将异常传递到链条上。
有任何改进的批评/建议吗?
代码包括测试代码:
#!/usr/bin/env python
from __future__ import print_function
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
class RunObjectContainer(QtCore.QObject):
#temporarily holds references so objects don't get garbage collected
def __init__(self):
self._container = set()
def add(self, obj):
self._container.add(obj)
@QtCore.pyqtSlot(object)
def discard(self, obj):
self._container.discard(obj)
container = RunObjectContainer()
class RunObject(QtCore.QObject):
run_complete = QtCore.pyqtSignal(object)
def __init__(self, parent=None,f=None):
super(RunObject, self).__init__(parent)
self._f = f
@QtCore.pyqtSlot()
def run(self):
self._f()
self.run_complete.emit(self)
main_thread = None
worker_thread = QtCore.QThread()
def run_on_thread(thread_to_use, f):
run_obj = RunObject(f=f)
container.add(run_obj)
run_obj.run_complete.connect(container.discard)
if QtCore.QThread.currentThread() != thread_to_use:
run_obj.moveToThread(thread_to_use)
QtCore.QMetaObject.invokeMethod(run_obj, 'run', QtCore.Qt.QueuedConnection)
def print_run_on(msg):
if QtCore.QThread.currentThread() == main_thread:
print(msg + " -- run on main thread")
elif QtCore.QThread.currentThread() == worker_thread:
print(msg + " -- run on worker thread")
else:
print("error " + msg + " -- run on unkown thread")
raise Exception(msg + " -- run on unkown thread")
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.initUI()
def initUI(self):
self.button = QtGui.QPushButton('Test', self)
self.button.clicked.connect(self.handleButton)
self.show()
def handleButton(self):
run_on_thread(main_thread, lambda: print_run_on("main_thread"))
run_on_thread(worker_thread, lambda: print_run_on("worker_thread"))
def n():
a = "yoyoyo"
print_run_on("running function n on thread ")
run_on_thread(main_thread, lambda: print_run_on("calling nested from n "))
run_on_thread(worker_thread, lambda: print_run_on("a is " + a))
run_on_thread(worker_thread, n)
print("end of handleButton")
def gui_main():
app = QtGui.QApplication(sys.argv)
ex = Example()
worker_thread.start()
global main_thread
main_thread = app.thread()
sys.exit(app.exec_())
if __name__ == '__main__':
gui_main()
编辑:有一个额外的抽象
添加
def run_on_thread_d(thread_to_use):
def decorator(func):
run_on_thread(thread_to_use, func)
return func
return decorator
你可以替换handleButton方法
def handleButton(self):
@run_on_thread_d(worker_thread)
def _():
a = "yoyoyo"
print_run_on("running function n on thread ")
@run_on_thread_d(main_thread)
def _():
print_run_on("calling nested from n ")
@run_on_thread_d(worker_thread)
def _():
print_run_on("a is " + a)