简单,无阻塞的后台调用

时间:2014-12-18 12:48:55

标签: python multithreading pyqt pyside

我正在编写一个通过串行连接与硬件通信的PySide应用程序。

我有一个启动设备命令的按钮和一个标签,用于向用户显示结果。 现在有些设备需要很长时间(多秒)才能回复请求,这会冻结GUI。我正在寻找一种简单的机制来在后台线程或类似的线程中运行调用。

我创建了一个我想要完成的简短示例:

import sys
import time
from PySide import QtCore, QtGui


class Device(QtCore.QObject):

    def request(self, cmd):
        time.sleep(3)
        return 'Result for {}'.format(cmd)


class Dialog(QtGui.QDialog):

    def __init__(self, device, parent=None):
        super().__init__(parent)
        self.device = device

        self.layout = QtGui.QHBoxLayout()
        self.label = QtGui.QLabel('--')
        self.button = QtGui.QPushButton('Go')
        self.layout.addWidget(self.label)
        self.layout.addWidget(self.button)
        self.setLayout(self.layout)

        self.button.clicked.connect(self.go)

    def go(self):
        self.button.setEnabled(False)

        # the next line should be called in the
        # background and not freeze the gui
        result = self.device.request('command')

        self.label.setText(result)
        self.button.setEnabled(True)


if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    dev = Device()
    win = Dialog(device=dev)
    win.show()
    win.raise_()
    app.exec_()

我希望拥有的是某种功能,如:

result = nonblocking(self.device.request, 'command')

应该提出异常,就好像我直接调用了函数一样。

任何想法或建议?

2 个答案:

答案 0 :(得分:1)

线程是最好的方法。 Python线程也很容易使用。 Qt线程与python线程的工作方式不同。 http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/

我只是使用python线程。此外,如果您使用串行端口连接,您可能希望将数据拆分为线程安全的队列。

import time
import threading
import queue
import serial

def read_serial(serial, storage):
    while True:
        value = serial.readline()
        storage.put(value) # or just process the data

ser = serial.SerailPort("Com1", 9600)
stor = queue.Queue()
th = threading.Thread(target=read_serial, args=(ser, stor))
th.start()
for _ in range(10):
    time.sleep(1)
th.join(0)
ser.close()
# Read queue

您可以做的另一件事是对串行端口和QObject使用多重继承。这允许您使用Qt信号。下面是一个非常粗略的例子。

class SerialThread(object):
    def __init__(self, port=None, baud=9600):
        super().__init__()

        self.state = threading.Condition() # Notify threading changes safely
        self.alive = threading.Event() # helps with locking

        self.data = queue.Queue()
        self.serial = serial.Serial()
        self.thread = threading.Thread(target=self._run)

        if port is not None:
            self.connect(port, baud)
    # end Constructor

    def connect(self, port, baud):
        self.serial.setPort(port)
        self.serial.setBaudrate(baud)
        self.serial.open()

        with self.state:
            self.alive.set()
        self.thread.start()
    # end connect

    def _run(self):
        while True:
            with self.state:
                if not self.alive.is_set():
                    return

            self.read()
    # end _run

    def read(self):
        serstring = bytes("", "ascii")
        try:
            serstring = self.serial.readline()
        except:
            pass
        else:
            self.process_read(serstring)
        return serstring # if called directly
    # end read

    def process_read(self, serstring):
        if self.queue.full():
            self.queue.get(0) # remove the first item to free up space
        self.queue.put(serstring)
    # end process_read

    def disconnect(self):
        with self.state:
            self.alive.clear()
            self.state.notify()
        self.thread.join(0) # Close the thread
        self.serial.close() # Close the serial port
    # end disconnect
# end class SerialThread

class SerialPort(SerialThread, QtCore.QObject):
    data_updated = QtCore.Signal()

    def process_read(self, serstring):
        super().process_read(serstring)
        self.data_updated.emit()
    # end process_read
# end class SerialPort


if __name__ == "__main__":
    ser = SerialPort()
    ser.connect("COM1", 9600)
    # Do something / wait / handle data
    ser.disconnect()
    ser.queue.get() # Handle data

始终确保在退出时正确关闭并断开所有连接。另请注意,线程只能运行一次,因此您可能需要查看可调用的线程示例How to start and stop thread?。 您也可以通过Qt信号发出数据,而不是使用队列来存储数据。

答案 1 :(得分:1)

  

我希望拥有的是某种功能,如:

    result = nonblocking(self.device.request, 'command')

你在这里要求的实际上是不可能的。你不能有一个非阻塞的呼叫立即返回结果!根据定义,这将是一个阻塞呼叫。

非阻塞调用看起来像:

self.thread = inthread(self.device.request, 'command', callback)

self.device.request在串行请求完成时运行callback函数/方法。但是,从线程中天真地调用callback()会使回调在线程中运行,如果你要从callback方法调用Qt GUI,这是非常非常糟糕的(Qt GUI方法是不是线程安全的)。因此,您需要一种在MainThread中运行回调的方法。

我对这些函数有类似的需求,并且(与同事)创建了一个库(称为qtutils),其中包含一些不错的包装函数(包括{{1的实现) }})。它的工作原理是使用inthread将事件发布到MainThread。该事件包含对要运行的函数的引用,并且事件处理程序(驻留在主线程中)执行该函数。因此,您的QApplication.postEvent()方法将如下所示:

request

其中def request(self, cmd, callback): time.sleep(3) inmain(callback, 'Result for {}'.format(cmd)) 向主线程发布事件并运行inmain()

可以找到此库的文档here,或者您也可以根据上面的大纲设置自己的文档。如果您选择使用我的库,可以通过pip或easy_install安装它。

注意:使用此方法的一个警告是callback(data) leaks memory in PySide。这就是我现在使用PyQt的原因!