像pyqt中的模式异步?或更清洁的背景通话模式?

时间:2014-07-11 03:47:42

标签: python qt pyqt nonblocking

我正在尝试编写一个响应的短(一个文件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一起正常工作?

如果有人知道另一种理智模式。这也是有用的/可接受的答案。

4 个答案:

答案 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 fromasyncio使异步编程更容易。我认为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

这里有一些恼人的限制:

  1. finished信号不易处理任意类型。您必须为要返回的每个类型显式声明和连接处理程序,然后在获得结果时确保emit到正确的处理程序。这称为信号过载。一些具有相同C ++签名won't behave right when used together like this的Python类型,例如pyqtSignal([dict], [list])。最终您可能更容易创建几个不同的QThread子类来处理您可以从线程中运行的东西返回的不同类型。
  2. 您必须保存对您创建的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尝试过。)

  1. 似乎有效
  2. 我想我明白它是如何运作的
  3. 它相当短,行数明智
  4. 基本上,您安排函数在特定线程上运行,并使用带有嵌套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)