Background thread with QThread in PyQt

后端 未结 6 1223
忘掉有多难
忘掉有多难 2020-11-22 05:39

I have a program which interfaces with a radio I am using via a gui I wrote in PyQt. Obviously one of the main functions of the radio is to transmit data, but to do this co

6条回答
  •  暗喜
    暗喜 (楼主)
    2020-11-22 06:16

    In PyQt there are a lot of options for getting asynchronous behavior. For things that need event processing (ie. QtNetwork, etc) you should use the QThread example I provided in my other answer on this thread. But for the vast majority of your threading needs, I think this solution is far superior than the other methods.

    The advantage of this is that the QThreadPool schedules your QRunnable instances as tasks. This is similar to the task pattern used in Intel's TBB. It's not quite as elegant as I like but it does pull off excellent asynchronous behavior.

    This allows you to utilize most of the threading power of Qt in Python via QRunnable and still take advantage of signals and slots. I use this same code in several applications, some that make hundreds of asynchronous REST calls, some that open files or list directories, and the best part is using this method, Qt task balances the system resources for me.

    import time
    from PyQt4 import QtCore
    from PyQt4 import QtGui
    from PyQt4.QtCore import Qt
    
    
    def async(method, args, uid, readycb, errorcb=None):
        """
        Asynchronously runs a task
    
        :param func method: the method to run in a thread
        :param object uid: a unique identifier for this task (used for verification)
        :param slot updatecb: the callback when data is receieved cb(uid, data)
        :param slot errorcb: the callback when there is an error cb(uid, errmsg)
    
        The uid option is useful when the calling code makes multiple async calls
        and the callbacks need some context about what was sent to the async method.
        For example, if you use this method to thread a long running database call
        and the user decides they want to cancel it and start a different one, the
        first one may complete before you have a chance to cancel the task.  In that
        case, the "readycb" will be called with the cancelled task's data.  The uid
        can be used to differentiate those two calls (ie. using the sql query).
    
        :returns: Request instance
        """
        request = Request(method, args, uid, readycb, errorcb)
        QtCore.QThreadPool.globalInstance().start(request)
        return request
    
    
    class Request(QtCore.QRunnable):
        """
        A Qt object that represents an asynchronous task
    
        :param func method: the method to call
        :param list args: list of arguments to pass to method
        :param object uid: a unique identifier (used for verification)
        :param slot readycb: the callback used when data is receieved
        :param slot errorcb: the callback used when there is an error
    
        The uid param is sent to your error and update callbacks as the
        first argument. It's there to verify the data you're returning
    
        After created it should be used by invoking:
    
        .. code-block:: python
    
           task = Request(...)
           QtCore.QThreadPool.globalInstance().start(task)
    
        """
        INSTANCES = []
        FINISHED = []
        def __init__(self, method, args, uid, readycb, errorcb=None):
            super(Request, self).__init__()
            self.setAutoDelete(True)
            self.cancelled = False
    
            self.method = method
            self.args = args
            self.uid = uid
            self.dataReady = readycb
            self.dataError = errorcb
    
            Request.INSTANCES.append(self)
    
            # release all of the finished tasks
            Request.FINISHED = []
    
        def run(self):
            """
            Method automatically called by Qt when the runnable is ready to run.
            This will run in a separate thread.
            """
            # this allows us to "cancel" queued tasks if needed, should be done
            # on shutdown to prevent the app from hanging
            if self.cancelled:
                self.cleanup()
                return
    
            # runs in a separate thread, for proper async signal/slot behavior
            # the object that emits the signals must be created in this thread.
            # Its not possible to run grabber.moveToThread(QThread.currentThread())
            # so to get this QObject to properly exhibit asynchronous
            # signal and slot behavior it needs to live in the thread that
            # we're running in, creating the object from within this thread
            # is an easy way to do that.
            grabber = Requester()
            grabber.Loaded.connect(self.dataReady, Qt.QueuedConnection)
            if self.dataError is not None:
                grabber.Error.connect(self.dataError, Qt.QueuedConnection)
    
            try:
                result = self.method(*self.args)
                if self.cancelled:
                    # cleanup happens in 'finally' statement
                    return
                grabber.Loaded.emit(self.uid, result)
            except Exception as error:
                if self.cancelled:
                    # cleanup happens in 'finally' statement
                    return
                grabber.Error.emit(self.uid, unicode(error))
            finally:
                # this will run even if one of the above return statements
                # is executed inside of the try/except statement see:
                # https://docs.python.org/2.7/tutorial/errors.html#defining-clean-up-actions
                self.cleanup(grabber)
    
        def cleanup(self, grabber=None):
            # remove references to any object or method for proper ref counting
            self.method = None
            self.args = None
            self.uid = None
            self.dataReady = None
            self.dataError = None
    
            if grabber is not None:
                grabber.deleteLater()
    
            # make sure this python obj gets cleaned up
            self.remove()
    
        def remove(self):
            try:
                Request.INSTANCES.remove(self)
    
                # when the next request is created, it will clean this one up
                # this will help us avoid this object being cleaned up
                # when it's still being used
                Request.FINISHED.append(self)
            except ValueError:
                # there might be a race condition on shutdown, when shutdown()
                # is called while the thread is still running and the instance
                # has already been removed from the list
                return
    
        @staticmethod
        def shutdown():
            for inst in Request.INSTANCES:
                inst.cancelled = True
            Request.INSTANCES = []
            Request.FINISHED = []
    
    
    class Requester(QtCore.QObject):
        """
        A simple object designed to be used in a separate thread to allow
        for asynchronous data fetching
        """
    
        #
        # Signals
        #
    
        Error = QtCore.pyqtSignal(object, unicode)
        """
        Emitted if the fetch fails for any reason
    
        :param unicode uid: an id to identify this request
        :param unicode error: the error message
        """
    
        Loaded = QtCore.pyqtSignal(object, object)
        """
        Emitted whenever data comes back successfully
    
        :param unicode uid: an id to identify this request
        :param list data: the json list returned from the GET
        """
    
        NetworkConnectionError = QtCore.pyqtSignal(unicode)
        """
        Emitted when the task fails due to a network connection error
    
        :param unicode message: network connection error message
        """
    
        def __init__(self, parent=None):
            super(Requester, self).__init__(parent)
    
    
    class ExampleObject(QtCore.QObject):
        def __init__(self, parent=None):
            super(ExampleObject, self).__init__(parent)
            self.uid = 0
            self.request = None
    
        def ready_callback(self, uid, result):
            if uid != self.uid:
                return
            print "Data ready from %s: %s" % (uid, result)
    
        def error_callback(self, uid, error):
            if uid != self.uid:
                return
            print "Data error from %s: %s" % (uid, error)
    
        def fetch(self):
            if self.request is not None:
                # cancel any pending requests
                self.request.cancelled = True
                self.request = None
    
            self.uid += 1
            self.request = async(slow_method, ["arg1", "arg2"], self.uid,
                                 self.ready_callback,
                                 self.error_callback)
    
    
    def slow_method(arg1, arg2):
        print "Starting slow method"
        time.sleep(1)
        return arg1 + arg2
    
    
    if __name__ == "__main__":
        import sys
        app = QtGui.QApplication(sys.argv)
    
        obj = ExampleObject()
    
        dialog = QtGui.QDialog()
        layout = QtGui.QVBoxLayout(dialog)
        button = QtGui.QPushButton("Generate", dialog)
        progress = QtGui.QProgressBar(dialog)
        progress.setRange(0, 0)
        layout.addWidget(button)
        layout.addWidget(progress)
        button.clicked.connect(obj.fetch)
        dialog.show()
    
        app.exec_()
        app.deleteLater() # avoids some QThread messages in the shell on exit
        # cancel all running tasks avoid QThread/QTimer error messages
        # on exit
        Request.shutdown()
    

    When exiting the application you'll want to make sure you cancel all of the tasks or the application will hang until every scheduled task has completed

提交回复
热议问题