我有一个程序,可以通过我在PyQt中编写的gui与正在使用的无线电接口。显然,无线电的主要功能之一是传输数据,但是要连续进行此操作,我必须循环写入,这会导致gui挂起。由于我从未处理过线程,因此我尝试使用QCoreApplication.processEvents().消除这些挂起。尽管,无线电需要在两次传输之间睡眠,所以gui仍然根据这些睡眠持续的时间来挂起。

是否有使用QThread修复此问题的简单方法?我一直在寻找有关如何使用PyQt实现多线程的教程,但是其中大多数都涉及设置服务器,并且比我需要的要先进得多。老实说,我什至不需要我的线程在它运行时更新任何东西,我只需要启动它,使其在后台传输,然后停止它即可。

#1 楼

我创建了一个小示例,展示了3种不同的简单线程处理方式。希望它能帮助您找到解决问题的正确方法。

import sys
import time

from PyQt5.QtCore import (QCoreApplication, QObject, QRunnable, QThread,
                          QThreadPool, pyqtSignal)


# Subclassing QThread
# http://qt-project.org/doc/latest/qthread.html
class AThread(QThread):

    def run(self):
        count = 0
        while count < 5:
            time.sleep(1)
            print("A Increasing")
            count += 1

# Subclassing QObject and using moveToThread
# http://blog.qt.digia.com/blog/2007/07/05/qthreads-no-longer-abstract
class SomeObject(QObject):

    finished = pyqtSignal()

    def long_running(self):
        count = 0
        while count < 5:
            time.sleep(1)
            print("B Increasing")
            count += 1
        self.finished.emit()

# Using a QRunnable
# http://qt-project.org/doc/latest/qthreadpool.html
# Note that a QRunnable isn't a subclass of QObject and therefore does
# not provide signals and slots.
class Runnable(QRunnable):

    def run(self):
        count = 0
        app = QCoreApplication.instance()
        while count < 5:
            print("C Increasing")
            time.sleep(1)
            count += 1
        app.quit()


def using_q_thread():
    app = QCoreApplication([])
    thread = AThread()
    thread.finished.connect(app.exit)
    thread.start()
    sys.exit(app.exec_())

def using_move_to_thread():
    app = QCoreApplication([])
    objThread = QThread()
    obj = SomeObject()
    obj.moveToThread(objThread)
    obj.finished.connect(objThread.quit)
    objThread.started.connect(obj.long_running)
    objThread.finished.connect(app.exit)
    objThread.start()
    sys.exit(app.exec_())

def using_q_runnable():
    app = QCoreApplication([])
    runnable = Runnable()
    QThreadPool.globalInstance().start(runnable)
    sys.exit(app.exec_())

if __name__ == "__main__":
    #using_q_thread()
    #using_move_to_thread()
    using_q_runnable()


评论


谢谢,这看起来确实很有用。如果使用QThread或QObject方法执行此操作,是否可以像创建self.finished信号一样添加更多自己的信号?例如,如果我不只是打印计数,而是要在另一个类的gui的QSpinBox中显示count的值。

– gwenger
2011年7月22日在13:34

是的,您可以添加自己的信号。一种实现方法是发射具有更新值的信号(例如pyqtSignal(int)),然后从您的GUI类连接到该信号,以相应地更新QSpinBox。

–aukaost
2011年7月22日在13:42

我不确定usingMoveToThread解决方案在我的情况下是否有效,我尝试取消对usingMoveToThread()的注释并使用usingQThread进行注释,但是当我运行脚本时,从不会将增加显示出来。

–ilpoldo
13年4月11日在12:12



我发现了一种使用PyQt 4.6修复它的奇怪方法。似乎QThread :: run()的调用不正确(我想这与QThread :: run()不再是纯虚函数有关)。这听起来有些愚蠢,但要解决它,只需创建自己的QThread子类,重新实现run()并填写QThread.run(self)。就是这样,它神奇地起作用

–马修·莱文(Matthew Levine)
13年5月22日在21:04

您建议一般使用哪一种?

–cxs1031
18年4月3日在15:38

#2 楼

将此更新为PyQt5,python 3.4的答案

将此模式用作启动不接受数据的工作程序并返回可用于表单的数据的模式。 1-使Worker类更小,并将其放在自己的文件worker.py中,以方便记忆和独立的软件重用。

2-main.py文件是定义GUI Form类的文件/>
3-线程对象未被子类化。

4-线程对象和辅助对象都属于Form对象

5-步骤步骤在注释内。

# worker.py
from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
import time


class Worker(QObject):
    finished = pyqtSignal()
    intReady = pyqtSignal(int)


    @pyqtSlot()
    def procCounter(self): # A slot takes no params
        for i in range(1, 100):
            time.sleep(1)
            self.intReady.emit(i)

        self.finished.emit()


主文件是:

  # main.py
  from PyQt5.QtCore import QThread
  from PyQt5.QtWidgets import QApplication, QLabel, QWidget, QGridLayout
  import sys
  import worker


  class Form(QWidget):

    def __init__(self):
       super().__init__()
       self.label = QLabel("0")

       # 1 - create Worker and Thread inside the Form
       self.obj = worker.Worker()  # no parent!
       self.thread = QThread()  # no parent!

       # 2 - Connect Worker`s Signals to Form method slots to post data.
       self.obj.intReady.connect(self.onIntReady)

       # 3 - Move the Worker object to the Thread object
       self.obj.moveToThread(self.thread)

       # 4 - Connect Worker Signals to the Thread slots
       self.obj.finished.connect(self.thread.quit)

       # 5 - Connect Thread started signal to Worker operational slot method
       self.thread.started.connect(self.obj.procCounter)

       # * - Thread finished signal will close the app if you want!
       #self.thread.finished.connect(app.exit)

       # 6 - Start the thread
       self.thread.start()

       # 7 - Start the form
       self.initUI()


    def initUI(self):
        grid = QGridLayout()
        self.setLayout(grid)
        grid.addWidget(self.label,0,0)

        self.move(300, 150)
        self.setWindowTitle('thread test')
        self.show()

    def onIntReady(self, i):
        self.label.setText("{}".format(i))
        #print(i)

    app = QApplication(sys.argv)

    form = Form()

    sys.exit(app.exec_())


评论


对于那些不知道的人,请参阅此问题,以了解为什么使用pyqtSlot()装饰器和此答案中概述的特定信号连接顺序很重要。

–三菠萝
16年4月18日在23:09

您真是天赐之物,谢谢,谢谢,感谢您强调工人和线程不需要父母的事实!我正在处理QThread:在过去三个小时内,线程仍在运行时被破坏了,然后我读了一下,然后单击!

–天际
5月1日0:45

花花公子...文学之神

– real_hagrid
5月7日8:07

#3 楼

Matt的一个很好的例子,我修正了错字,而且pyqt4.8现在很常见,因此我也删除了哑类,并为dataReady信号添加了一个例子

评论


非常好。这应该是当今公认的答案!

– Trilarion
15年1月8日在8:51

QMetaObject丑陋

– astrojuanlu
2015年4月7日14:41

是的,QMetaObject就像罪恶一样丑陋,但在Qt的信号和插槽利用的内幕之下。这允许调用者以线程安全的方式将消息有效地添加到Worker的信号队列中。看看我也使用QRunnables的答案。它也可能有其自身的缺陷,但是在您的PyQt应用程序中获得异步行为非常强大。它还不使用QMetaObject

–马修·莱文(Matthew Levine)
16年2月23日在16:15

从qthread继承显然可以:woboq.com/blog/qthread-you-were-not-doing-so-wrong.html

–尼尔G
16-09-18在22:20

不,这不应是公认的答案。当您可以简单地定义适当的信号插槽连接时,绝对没有充分的理由来利用基于QMetaObject的技巧,这些技巧以整个挥手留言墙为前缀。

– Cecil咖喱
18-3-29在5:45



#4 楼

根据Qt开发人员的说法,对QThread的子类化是不正确的(请参见http://blog.qt.io/blog/2010/06/17/youre-doing-it-wrong/)。但是那篇文章真的很难理解(加上标题有点居高临下)。我找到了一篇更好的博客文章,其中详细说明了为什么您应该在另一种样式上使用一种线程样式:http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-在我看来,qthreads-the-full-explanation /

绝不应该为重载run方法而对线程进行子类化。尽管这确实有效,但您基本上是在规避Qt希望您如何工作。另外,您将错过事件,适当的线程安全信号和插槽之类的东西。另外,您可能会在上面的博客文章中看到,“正确的”线程化方式迫使您编写更多可测试的代码。 (我在下面发布了一个单独的答案,该答案正确使用QRunnable并合并了信号/插槽,如果您有很多需要负载均衡的异步任务,那么答案会更好。)

import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import Qt

# very testable class (hint: you can use mock.Mock for the signals)
class Worker(QtCore.QObject):
    finished = QtCore.pyqtSignal()
    dataReady = QtCore.pyqtSignal(list, dict)

    @QtCore.pyqtSlot()
    def processA(self):
        print "Worker.processA()"
        self.finished.emit()

    @QtCore.pyqtSlot(str, list, list)
    def processB(self, foo, bar=None, baz=None):
        print "Worker.processB()"
        for thing in bar:
            # lots of processing...
            self.dataReady.emit(['dummy', 'data'], {'dummy': ['data']})
        self.finished.emit()


class Thread(QtCore.QThread):
    """Need for PyQt4 <= 4.6 only"""
    def __init__(self, parent=None):
        QtCore.QThread.__init__(self, parent)

     # this class is solely needed for these two methods, there
     # appears to be a bug in PyQt 4.6 that requires you to
     # explicitly call run and start from the subclass in order
     # to get the thread to actually start an event loop

    def start(self):
        QtCore.QThread.start(self)

    def run(self):
        QtCore.QThread.run(self)


app = QtGui.QApplication(sys.argv)

thread = Thread() # no parent!
obj = Worker() # no parent!
obj.moveToThread(thread)

# if you want the thread to stop after the worker is done
# you can always call thread.start() again later
obj.finished.connect(thread.quit)

# one way to do it is to start processing as soon as the thread starts
# this is okay in some cases... but makes it harder to send data to
# the worker object from the main gui thread.  As you can see I'm calling
# processA() which takes no arguments
thread.started.connect(obj.processA)
thread.start()

# another way to do it, which is a bit fancier, allows you to talk back and
# forth with the object in a thread safe way by communicating through signals
# and slots (now that the thread is running I can start calling methods on
# the worker object)
QtCore.QMetaObject.invokeMethod(obj, 'processB', Qt.QueuedConnection,
                                QtCore.Q_ARG(str, "Hello World!"),
                                QtCore.Q_ARG(list, ["args", 0, 1]),
                                QtCore.Q_ARG(list, []))

# that looks a bit scary, but its a totally ok thing to do in Qt,
# we're simply using the system that Signals and Slots are built on top of,
# the QMetaObject, to make it act like we safely emitted a signal for 
# the worker thread to pick up when its event loop resumes (so if its doing
# a bunch of work you can call this method 10 times and it will just queue
# up the calls.  Note: PyQt > 4.6 will not allow you to pass in a None
# instead of an empty list, it has stricter type checking

app.exec_()

# Without this you may get weird QThread messages in the shell on exit
app.deleteLater()        


评论


从另一个Qt开发人员那里,woboq.com / blog / qthread-you-were-not-doing-so-wrong.html的子类化在仅实现run方法时完全可以接受。

–simotek
15年3月4日在23:36

您似乎可以指出该问题在PyQt 4.6中是一个错误,所以我们知道是否/何时解决。

–破破烂烂
15年9月24日在23:19

为Posch文章的链接提供支持,因为它可以清除冲突的视图。

–迈克尔·谢珀(Michael Scheper)
2015年12月24日,0:06

从qthread继承显然可以:woboq.com/blog/qthread-you-were-not-doing-so-wrong.html

–尼尔G
16-09-18在22:20

您的链接已过时,您可能需要更新:blog.qt.io/blog/2010/06/17/youre-doing-it-wrong

–塞尔诺
18-11-19在15:33



#5 楼

在PyQt中,有很多获取异步行为的选项。对于需要事件处理的事物(即QtNetwork等),您应该使用我在此线程的其他答案中提供的QThread示例。但是对于您绝大多数的线程需求,我认为该解决方案比其他方法要优越得多。

优点是QThreadPool将QRunnable实例调度为任务。这类似于英特尔的TBB中使用的任务模式。它并不像我所希望的那么优雅,但是它确实带来了出色的异步行为。我在几个应用程序中使用了相同的代码,一些应用程序进行了数百个异步REST调用,一些应用程序打开了文件或列表目录,而最好的部分是使用此方法,Qt任务为我平衡了系统资源。

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()


退出应用程序时,您需要确保取消所有任务,否则应用程序将挂起,直到每个计划的任务完成为止。

评论


这是一个很好的答案-谢谢!有一篇博客文章《使用QThreadPool实现多线程PyQt应用程序》具有类似的方法。

–菲尔
17年11月16日在20:36



#6 楼

基于其他答案中提到的Worker对象方法,我决定看看是否可以扩展解决方案以调用更多线程-在这种情况下,机器可以运行的最佳数量并以不确定的完成时间启动多个worker。 >为此,我仍然需要继承QThread-但仅分配一个线程号并“重新实现”信号“完成”和“开始”以包括其线程号。

我集中精力类似地,其他答案也让人指出,不要将QThread作为父母,这很痛苦,但是我不认为这是一个错误。真正的关注。但是,我的代码也小心翼翼地销毁了QThread对象。

但是,我无法将worker对象作为父对象,因此似乎希望在线程执行操作时向它们发送deleteLater()信号功能完成或GUI被破坏。我没有执行此操作就挂起了自己的代码。 GUI将等待所有线程完成。当我用这个问题的其他一些答案玩耍时,我发现QThread销毁了错误。

也许对其他人有用。我当然发现它是一个有用的练习。也许其他人会知道线程声明其身份的更好方法。