Kommunikation mit und zurück von einem Thread

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Hellstorm
User
Beiträge: 231
Registriert: Samstag 22. Juni 2013, 15:01

Hallo,

wie kann ich vernünftig mit einem Thread kommunizieren, so dass Benutzernachfragen verarbeitet werden?

Ein Beispiel:
Hauptprogramm läuft, im Hintergrund macht ein Thread irgendetwas. Während des Ablaufs gibt es ein Problem, zu dem der Benutzer gefragt werden soll. Der Benutzer bekommt einen Fragedialog zu Gesicht und drückt auf Ja oder Nein. Anschließend macht der Thread je nach Antwort weiter.

Also ich habe es jetzt wohl so verstanden, dass man da eine Eventloop in den Thread reinschreiben muss. Nur wie genau mache ich die Kommunikation und wie kann ich weitermachen? Das ist mir nicht so ganz eindeutig.

Ich habe jetzt mal als Test folgendes geschrieben:

Code: Alles auswählen

class TestThread(QtCore.QThread):
    ask_question = QtCore.pyqtSignal()

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

    def run(self):
        print("Starte Eventloop")
        time.sleep(10)
        self.ask_question.emit()
        self.exec_()
Aber wie genau würde ich da jetzt die Antwort-Verarbeitung einbauen? Ich muss einen Slot erstellen, denke ich. Der soll dann die Eventloop wieder beenden, oder wie genau geht das?

Ich bedanke mich schon einmal herzlich :)
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Hellstorm:
Was Du beschreibst, funktioniert mit Signal/Slots in Richtung Mainthread(Signal)-->Subthread(Slot) nur dann, wenn der Thread bzw. die Aufgaben selbst ereignisgesteuert sind, hier mal ein Beispiel:

Code: Alles auswählen

from PyQt4 import QtCore

class Task(QtCore.QObject):
    question = QtCore.pyqtSignal(str)
    def start(self):
        output(QtCore.QThread.currentThreadId(), 'task-start')
        self.question.emit('How do you feel?')
    def process_answer(self, data):
        output(QtCore.QThread.currentThreadId(), 'task-process_answer')
        output('Subthread reads:', data)

class EventThread(QtCore.QThread):
    def run(self):
        output(QtCore.QThread.currentThreadId(), 'thread-run')
        self.exec_()


class Frontend(QtCore.QObject):
    answer = QtCore.pyqtSignal(str)
    start = QtCore.pyqtSignal()
    def __init__(self, parent=None):
        QtCore.QObject.__init__(self, parent)
        output(QtCore.QThread.currentThreadId(), 'frontend-init')
    def process_question(self, data):
        output(QtCore.QThread.currentThreadId(), 'frontend-process_question')
        output('question from thread:', data)
        self.answer.emit('Good!') # input goes here

def _output():
    m = QtCore.QMutex()
    def wrapped(*it):
        m.lock()
        print ' '.join(str(i) for i in it)
        m.unlock()
    return wrapped
output = _output()

if __name__ == '__main__':
    app = QtCore.QCoreApplication([])

    # some frontend, e.g. a GUI
    frontend = Frontend()

    thread = EventThread()

    # some event driven task for the thread
    task = Task()
    task.moveToThread(thread)

    # connect question/answer ping pong between frontend and the task
    task.question.connect(frontend.process_question)
    frontend.answer.connect(task.process_answer)

    # start the thread
    # NOTE: task should start AFTER the thread's event loop is running
    # otherwise early signals get lost
    thread.started.connect(frontend.start)
    frontend.start.connect(task.start)
    thread.start()

    # tier down all event loops
    QtCore.QTimer.singleShot(100, lambda: all([thread.exit(), thread.wait(), app.exit()]))

    app.exec_()
Wie sieht denn Deine Prozesslogik im Thread aus? Dein Beispiel hat keinerlei Objekte im Subthreadkontext, welche die Slots liefern könnten. In der Tat musst Du einen Slot erstellen, nur sehe ich kein Objekt, über welches Du das machen könntest (siehe hierzu `Task` in meinem Bsp.) Beachte, dass Du nur innerhalb von `run()` im Subthread bist, alles andere, z.B. ein zusätzlicher Slot am Threadobjekt liegen im Mainthread.

Kann es sein, dass Du in `run()` einfach an der Stelle des Signals die Verarbeitung anhalten willst bis die Antwort vorliegt und dann entsprechend weitarbeiten willst? Das ginge in der Tat mit einem Slot am Threadobjekt, welcher Daten über einen Lock dem Thread zur Verfügung stellt:

Code: Alles auswählen

from PyQt4 import QtCore
from contextlib import contextmanager


class MyThread(QtCore.QThread):
    question = QtCore.pyqtSignal(str)
    def __init__(self, parent=None):
        QtCore.QThread.__init__(self, parent)
        self.data = ''
        self.mutex = QtCore.QMutex()

    @contextmanager
    def lock(self):
        self.mutex.lock()
        yield
        self.mutex.unlock()

    def run(self):
        print QtCore.QThread.currentThreadId(), 'thread-run'
        print 'wait for slot execution...'
        self.question.emit('Wots going on?') # thread pauses here
        print '...thread goes on'
        with self.lock():
            print self.data

    def process_answer(self, data):
        print QtCore.QThread.currentThreadId(), 'thread-process_answer'
        with self.lock():
            self.data = data


class Frontend(QtCore.QObject):
    answer = QtCore.pyqtSignal(str)
    def __init__(self, parent=None):
        QtCore.QObject.__init__(self, parent)
        print QtCore.QThread.currentThreadId(), 'main-init'

    def process_question(self, data):
        print QtCore.QThread.currentThreadId(), 'main-process_question'
        print data
        self.answer.emit('Nothing!')


if __name__ == '__main__':
    app = QtCore.QCoreApplication([])
    frontend = Frontend()
    thread = MyThread()
    thread.question.connect(frontend.process_question, QtCore.Qt.BlockingQueuedConnection)
    frontend.answer.connect(thread.process_answer)
    thread.start()
    QtCore.QTimer.singleShot(100, lambda: app.exit())
    app.exec_()
Dieses Bsp. kommt ohne Ereignisschleife im Thread aus. Ob der Lock nötig ist, hängt vom Datentypen und den Zugriffen ab. Alternativ zu Slot + Lock könnte man auch auf `Queue` aus dem threading-Modul zurückgreifen.
BlackJack

Bevor man sich grössere Sachen mit QThread selber bastelt, sollte man IMHO mal schauen ob `QtConcurrent` etwas passendes bietet.
Antworten