Hintergrund-Thread mit QThread in PyQt

77

Ich habe ein Programm, das über eine GUI, die ich in PyQt geschrieben habe, mit einem Radio verbunden ist, das ich verwende. Natürlich besteht eine der Hauptfunktionen des Radios darin, Daten zu übertragen, aber um dies kontinuierlich zu tun, muss ich die Schreibvorgänge schleifen, wodurch die GUI hängen bleibt. Da ich mich noch nie mit Threading befasst habe, habe ich versucht, diese Hänge mit zu beseitigen. QCoreApplication.processEvents().Das Radio muss jedoch zwischen den Übertragungen schlafen, sodass die GUI immer noch hängt, je nachdem, wie lange diese Schlafzeiten dauern.

Gibt es eine einfache Möglichkeit, dies mit QThread zu beheben? Ich habe nach Tutorials zur Implementierung von Multithreading mit PyQt gesucht, aber die meisten befassen sich mit der Einrichtung von Servern und sind viel weiter fortgeschritten, als ich es brauche. Ich brauche meinen Thread ehrlich gesagt nicht einmal wirklich, um etwas zu aktualisieren, während er ausgeführt wird. Ich muss ihn nur starten, im Hintergrund übertragen und stoppen.

Gwenger
quelle

Antworten:

145

Ich habe ein kleines Beispiel erstellt, das drei verschiedene und einfache Arten des Umgangs mit Threads zeigt. Ich hoffe, es wird Ihnen helfen, den richtigen Ansatz für Ihr Problem zu finden.

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()
aukaost
quelle
Danke, das sieht auf jeden Fall nützlich aus. Kann ich, wenn ich entweder die QThread- oder die QObject-Methode verwende, mehr eigene Signale hinzufügen, so wie Sie das self.finishedSignal erzeugt haben? Wenn ich beispielsweise nicht nur die Anzahl drucken möchte, möchte ich den Wert der Anzahl in einer QSpinBox anzeigen, die Teil meiner Benutzeroberfläche der anderen Klasse ist.
Gwenger
2
Ja, Sie können Ihre eigenen Signale hinzufügen. Eine Möglichkeit besteht darin, ein Signal (z. B. pyqtSignal (int)) mit dem aktualisierten Wert auszugeben und von Ihrer GUI-Klasse aus eine Verbindung herzustellen, um die QSpinBox entsprechend zu aktualisieren.
Aukaost
Ich bin nicht sicher, ob die usingMoveToThread-Lösung in meinem Fall funktioniert. Ich habe versucht usingMoveToThread(), mit QThread zu kommentieren und zu kommentieren, aber wenn ich das Skript Steigern ausführe, wird das Erhöhen nie auf dem Terminal ausgedruckt.
Ilpoldo
1
Ich habe einen seltsamen Weg gefunden, dies mit PyQt 4.6 zu beheben. Es scheint, dass QThread :: run () nicht korrekt aufgerufen wird (ich vermute, dies hat etwas damit zu tun, dass QThread :: run () keine reine virtuelle Funktion mehr ist). Dies wird dumm klingen, aber um es zu beheben, erstellen Sie einfach Ihre eigene Unterklasse von QThread, implementieren Sie run () neu und füllen Sie QThread.run (self) aus. Das ist es, und es funktioniert auf magische Weise
Matthew Levine
2
Welches schlagen Sie für den allgemeinen Gebrauch vor?
cxs1031
50

Nehmen Sie diese Antwort aktualisiert für PyQt5, Python 3.4

Verwenden Sie dies als Muster, um einen Worker zu starten, der keine Daten nimmt und Daten zurückgibt, sobald diese für das Formular verfügbar sind.

1 - Die Worker-Klasse wird verkleinert und in eine eigene Datei worker.py eingefügt, um das Speichern und die unabhängige Wiederverwendung von Software zu vereinfachen.

2 - Die Datei main.py ist die Datei, die die GUI-Formularklasse definiert

3 - Das Thread-Objekt ist nicht in Unterklassen unterteilt.

4 - Sowohl das Thread-Objekt als auch das Worker-Objekt gehören zum Form-Objekt

5 - Schritte des Verfahrens sind in den Kommentaren enthalten.

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

Und die Hauptdatei ist:

  # 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_())
Raiden Core
quelle
3
In dieser Frage erfahren Sie, warum die Verwendung des Dekorators pyqtSlot () `und die in dieser Antwort beschriebene spezifische Reihenfolge der Signalverbindung von Bedeutung sind.
Drei_Pineapples
1
Du bist ein Glücksfall, danke, danke, danke, dass du die Tatsache betont hast , dass der Arbeiter und der Faden keine Eltern haben müssen! Ich habe mich in QThread: Destroyed while thread is still runningden letzten drei Stunden damit beschäftigt, dann habe ich das gelesen und es hat geklickt!
Skybbles
Alter ... buchstäblicher Gott
real_hagrid
36

Sehr schönes Beispiel von Matt, ich habe den Tippfehler behoben und auch pyqt4.8 ist jetzt üblich, also habe ich auch die Dummy-Klasse entfernt und ein Beispiel für das dataReady-Signal hinzugefügt

# -*- coding: utf-8 -*-
import sys
from PyQt4 import QtCore, 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()


def onDataReady(aList, aDict):
    print 'onDataReady'
    print repr(aList)
    print repr(aDict)


app = QtGui.QApplication(sys.argv)

thread = QtCore.QThread()  # no parent!
obj = Worker()  # no parent!
obj.dataReady.connect(onDataReady)

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.finished.connect(app.exit)

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_()
user178047
quelle
1
Sehr schön. Dies sollte heutzutage die akzeptierte Antwort sein!
Trilarion
9
Das QMetaObjectist hässlich wie die Hölle
Astrojuanlu
Ja, das QMetaObject ist hässlich wie Sünde, aber unter der Haube nutzen Qts Signale und Slots das. Auf diese Weise kann der Anrufer der Signalwarteschlange des Arbeitnehmers auf threadsichere Weise effektiv eine Nachricht hinzufügen. Schauen Sie sich auch meine Antwort für die Verwendung von QRunnables an. Es kann auch seine eigenen Fehler haben, aber es ist sehr leistungsfähig, um asynchrones Verhalten in Ihrer PyQt-App zu erhalten. Es verwendet auch nicht QMetaObject
Matthew Levine
Das Erben von qthread ist anscheinend in Ordnung: Woboq.com/blog/qthread-you-were-not-doing-so-wrong.html
Neil G
2
Nein, dies sollte nicht die akzeptierte Antwort sein. Es gibt absolut keinen triftigen Grund, QMetaObjectHacks auf Basis von Hebel zu nutzen, denen eine ganze Kommentarwand mit Handwinken vorangestellt ist, wenn Sie stattdessen einfach die richtigen Signal-Slot-Verbindungen definieren könnten. </sigh>
Cecil Curry
30

Laut den Qt-Entwicklern ist die Unterklasse von QThread falsch (siehe http://blog.qt.io/blog/2010/06/17/youre-doing-it-wrong/ ). Aber dieser Artikel ist wirklich schwer zu verstehen (und der Titel ist etwas herablassend). Ich habe einen besseren Blog-Beitrag gefunden, der eine detailliertere Erklärung darüber enthält, warum Sie einen Threading-Stil über einen anderen verwenden sollten: http://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use- qthreads-the-full-erklärung /

Meiner Meinung nach sollten Sie Threads wahrscheinlich niemals in Unterklassen unterteilen, um die Ausführungsmethode zu überladen. Während das funktioniert, umgehen Sie im Grunde, wie Qt möchte, dass Sie arbeiten. Außerdem verpassen Sie Dinge wie Ereignisse und geeignete threadsichere Signale und Slots. Wie Sie wahrscheinlich im obigen Blog-Beitrag sehen werden, zwingt Sie die "richtige" Art des Threading dazu, mehr testbaren Code zu schreiben.

Hier sind einige Beispiele, wie Sie QThreads in PyQt nutzen können (Ich habe unten eine separate Antwort veröffentlicht, die QRunnable ordnungsgemäß verwendet und Signale / Slots enthält. Diese Antwort ist besser, wenn Sie viele asynchrone Aufgaben haben, die Sie zum Lastausgleich benötigen.) .

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()        
Matthew Levine
quelle
2
Von einem anderen Qt Dev woboq.com/blog/qthread-you-were-not-doing-so-wrong.html Unterklassen QThread ist vollkommen akzeptabel, wenn nur die Ausführungsmethode implementiert wird.
Simotek
appears to be a bug in PyQt 4.6Können Sie auf das Problem hinweisen, damit wir wissen, ob / wann es behoben ist?
Bindestrich
Upvoting dies für den Link zum Posch-Artikel, da es die widersprüchlichen Ansichten aufklärt.
Michael Scheper
1
Das Erben von qthread ist anscheinend in Ordnung: Woboq.com/blog/qthread-you-were-not-doing-so-wrong.html
Neil G
1
Ihr Link ist veraltet, Sie können ihn aktualisieren: blog.qt.io/blog/2010/06/17/youre-doing-it-wrong
Cerno
9

In PyQt gibt es viele Optionen, um asynchrones Verhalten zu erhalten. Für Dinge, die eine Ereignisverarbeitung erfordern (z. B. QtNetwork usw.), sollten Sie das QThread-Beispiel verwenden, das ich in meiner anderen Antwort auf diesen Thread angegeben habe. Aber für die überwiegende Mehrheit Ihrer Threading-Anforderungen denke ich, dass diese Lösung den anderen Methoden weit überlegen ist.

Dies hat den Vorteil, dass der QThreadPool Ihre QRunnable-Instanzen als Aufgaben plant. Dies ähnelt dem Aufgabenmuster, das in Intels TBB verwendet wird. Es ist nicht ganz so elegant wie ich es mag, aber es zeigt ein ausgezeichnetes asynchrones Verhalten.

Auf diese Weise können Sie den größten Teil der Threading-Leistung von Qt in Python über QRunnable nutzen und dennoch die Vorteile von Signalen und Slots nutzen. Ich verwende denselben Code in mehreren Anwendungen, von denen einige Hunderte von asynchronen REST-Aufrufen ausführen, andere Dateien öffnen oder Verzeichnisse auflisten. Das Beste daran ist, dass diese Methode verwendet wird. Die Qt-Task gleicht die Systemressourcen für mich aus.

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

Wenn Sie die Anwendung beenden, sollten Sie sicherstellen, dass Sie alle Aufgaben abbrechen. Andernfalls bleibt die Anwendung hängen, bis alle geplanten Aufgaben abgeschlossen sind

Matthew Levine
quelle
1
Dies ist eine ausgezeichnete Antwort - DANKE! Es gibt einen Blog-Beitrag Multithreading PyQt-Anwendungen mit QThreadPool , der einen ähnlichen Ansatz verfolgt.
Phil
5

Basierend auf den in anderen Antworten erwähnten Worker-Objektmethoden habe ich mich entschlossen, zu prüfen, ob ich die Lösung erweitern kann, um mehr Threads aufzurufen - in diesem Fall die optimale Anzahl, die die Maschine ausführen und mehrere Worker mit unbestimmten Abschlusszeiten hochfahren kann. Dazu muss ich noch QThread unterordnen - aber nur, um eine Thread-Nummer zuzuweisen und die Signale "beendet" und "gestartet" neu zu implementieren, um ihre Thread-Nummer einzuschließen.

Ich habe mich ziemlich auf die Signale zwischen der Haupt-GUI, den Threads und den Arbeitern konzentriert.

In ähnlicher Weise war es schwierig, andere Antworten darauf hinzuweisen, dass der QThread nicht erzogen wurde, aber ich denke nicht, dass dies ein echtes Problem ist. Mein Code achtet jedoch auch darauf, die QThread-Objekte zu zerstören.

Die Worker-Objekte konnten jedoch nicht übergeordnet werden, sodass es wünschenswert erscheint, ihnen das deleteLater () -Signal zu senden, entweder wenn die Thread-Funktion beendet oder die GUI zerstört ist. Ich habe meinen eigenen Code hängen lassen, weil ich das nicht getan habe.

Eine weitere Verbesserung, die ich für notwendig hielt, war die Neuimplementierung des closeEvent der GUI (QWidget), sodass die Threads zum Beenden aufgefordert wurden und die GUI dann warten musste, bis alle Threads fertig waren. Als ich mit einigen anderen Antworten auf diese Frage spielte, bekam ich QThread zerstörte Fehler.

Vielleicht ist es für andere nützlich. Ich fand es auf jeden Fall eine nützliche Übung. Vielleicht kennen andere einen besseren Weg, wie ein Thread seine Identität ankündigen kann.

#!/usr/bin/env python3
#coding:utf-8
# Author:   --<>
# Purpose:  To demonstrate creation of multiple threads and identify the receipt of thread results
# Created: 19/12/15

import sys


from PyQt4.QtCore import QThread, pyqtSlot, pyqtSignal
from PyQt4.QtGui import QApplication, QLabel, QWidget, QGridLayout

import sys
import worker

class Thread(QThread):
    #make new signals to be able to return an id for the thread
    startedx = pyqtSignal(int)
    finishedx = pyqtSignal(int)

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

        self.started.connect(self.starttt)
        self.finished.connect(self.finisheddd)

    @pyqtSlot()
    def starttt(self):
        print('started signal from thread emitted')
        self.startedx.emit(self.idd) 

    @pyqtSlot()
    def finisheddd(self):
        print('finished signal from thread emitted')
        self.finishedx.emit(self.idd)

class Form(QWidget):

    def __init__(self):
        super().__init__()

        self.initUI()

        self.worker={}
        self.threadx={}
        self.i=0
        i=0

        #Establish the maximum number of threads the machine can optimally handle
        #Generally relates to the number of processors

        self.threadtest = QThread(self)
        self.idealthreadcount = self.threadtest.idealThreadCount()

        print("This machine can handle {} threads optimally".format(self.idealthreadcount))

        while i <self.idealthreadcount:
            self.setupThread(i)
            i+=1

        i=0
        while i<self.idealthreadcount:
            self.startThread(i)
            i+=1

        print("Main Gui running in thread {}.".format(self.thread()))


    def setupThread(self,i):

        self.worker[i]= worker.Worker(i)  # no parent!
        #print("Worker object runningt in thread {} prior to movetothread".format(self.worker[i].thread()) )
        self.threadx[i] = Thread(i,parent=self)  #  if parent isn't specified then need to be careful to destroy thread 
        self.threadx[i].setObjectName("python thread{}"+str(i))
        #print("Thread object runningt in thread {} prior to movetothread".format(self.threadx[i].thread()) )
        self.threadx[i].startedx.connect(self.threadStarted)
        self.threadx[i].finishedx.connect(self.threadFinished)

        self.worker[i].finished.connect(self.workerFinished)
        self.worker[i].intReady.connect(self.workerResultReady)

        #The next line is optional, you may want to start the threads again without having to create all the code again.
        self.worker[i].finished.connect(self.threadx[i].quit)

        self.threadx[i].started.connect(self.worker[i].procCounter)

        self.destroyed.connect(self.threadx[i].deleteLater)
        self.destroyed.connect(self.worker[i].deleteLater)

        #This is the key code that actually get the worker code onto another processor or thread.
        self.worker[i].moveToThread(self.threadx[i])

    def startThread(self,i):
        self.threadx[i].start()

    @pyqtSlot(int)
    def threadStarted(self,i):
        print('Thread {}  started'.format(i))
        print("Thread priority is {}".format(self.threadx[i].priority()))        


    @pyqtSlot(int)
    def threadFinished(self,i):
        print('Thread {} finished'.format(i))




    @pyqtSlot(int)
    def threadTerminated(self,i):
        print("Thread {} terminated".format(i))

    @pyqtSlot(int,int)
    def workerResultReady(self,j,i):
        print('Worker {} result returned'.format(i))
        if i ==0:
            self.label1.setText("{}".format(j))
        if i ==1:
            self.label2.setText("{}".format(j))
        if i ==2:
            self.label3.setText("{}".format(j))
        if i ==3:
            self.label4.setText("{}".format(j)) 

        #print('Thread {} has started'.format(self.threadx[i].currentThreadId()))    

    @pyqtSlot(int)
    def workerFinished(self,i):
        print('Worker {} finished'.format(i))

    def initUI(self):
        self.label1 = QLabel("0")
        self.label2= QLabel("0")
        self.label3= QLabel("0")
        self.label4 = QLabel("0")
        grid = QGridLayout(self)
        self.setLayout(grid)
        grid.addWidget(self.label1,0,0)
        grid.addWidget(self.label2,0,1) 
        grid.addWidget(self.label3,0,2) 
        grid.addWidget(self.label4,0,3) #Layout parents the self.labels

        self.move(300, 150)
        self.setGeometry(0,0,300,300)
        #self.size(300,300)
        self.setWindowTitle('thread test')
        self.show()

    def closeEvent(self, event):
        print('Closing')

        #this tells the threads to stop running
        i=0
        while i <self.idealthreadcount:
            self.threadx[i].quit()
            i+=1

         #this ensures window cannot be closed until the threads have finished.
        i=0
        while i <self.idealthreadcount:
            self.threadx[i].wait() 
            i+=1        


        event.accept()


if __name__=='__main__':
    app = QApplication(sys.argv)
    form = Form()
    sys.exit(app.exec_())

Und der Arbeitercode unten

#!/usr/bin/env python3
#coding:utf-8
# Author:   --<>
# Purpose:  Stack Overflow
# Created: 19/12/15

import sys
import unittest


from PyQt4.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
import time
import random


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

    def __init__(self, i=0):
        '''__init__ is called while the worker is still in the Gui thread. Do not put slow or CPU intensive code in the __init__ method'''

        super().__init__()
        self.idd = i



    @pyqtSlot()
    def procCounter(self): # This slot takes no params
        for j in range(1, 10):
            random_time = random.weibullvariate(1,2)
            time.sleep(random_time)
            self.intReady.emit(j,self.idd)
            print('Worker {0} in thread {1}'.format(self.idd, self.thread().idd))

        self.finished.emit(self.idd)


if __name__=='__main__':
    unittest.main()
cmoman
quelle