Multithreading in PyQt: Problem locking

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Shaldy
User
Beiträge: 123
Registriert: Sonntag 2. März 2008, 22:49

Hallo zusammen.

In meinem PyQt Programm laufen 2 Threads, die teilweise auf die selben Variablen/Daten zugreifen. Das Problem dabei habe ich verstanden und auch, wie es mit den locks im thread-modul funktioniert.
Da ich aber QThread benutze weiß ich nicht, wie das Problem unter PyQt behandelt wird. Macht PyQt das automatisch oder muss ich da auch solche locks setzen?
Habe keine Klasse in der Dokumentation gefunden, die mir irgendwie weitergeholfen hat.
Dies ist keine Signatur!
lunar

Was sollte PyQt „automatisch machen“? Falls Du meinst, der gemeinsame Zugriff auf ein Objekt aus verschiedenen Threads wäre sicher, so kommt das auf den Typ des Objekts an. Manche Objekte sind thread-sicher, andere nicht. Das aber steht in der Dokumentation.
Shaldy
User
Beiträge: 123
Registriert: Sonntag 2. März 2008, 22:49

Okay, danke! Hab soweit auch keine Probleme, nur kommt jetzt ein Fehler, von dem ich absolut keine Ahnung habe, woran das liegt, deshalb kann ich jetzt auch schlecht den Code mitliefern...
Der Fehler lautet:

QObject::connect: Cannot queue arguments of type 'QItemSelection'
(Make sure 'QItemSelection' is registered using qRegisterMetaType().)

Was bedeutet das, und warum kommt der Fehler nicht jedes Mal an der entsprechenden Stelle im Programm?
Dies ist keine Signatur!
lunar

@Shaldy: Du kannst bei Signal-Slot-Verbindungen über Threadgrenzen nicht beliebige Typen übergeben, sondern nur eine gewisse Auswahl an Qt-Typen, sowie Python-Typen, die in solche Qt-Typen umgesetzt werden.

Die Gründe dazu stehen in der Dokumentation. Ich möchte Dir auch nicht zu nahe treten, aber ich bitte Dich, diese doch bitte auch zur Gänze zu lesen, bevor Du Komponenten aus Qt verwendest. Die Referenz der QThread-Klasse führt Dich zum Artikel über Thread Support in Qt, dessen Lektüre obligatorisch ist, wenn Du mit Qt und Threads arbeiten möchtest. Im Abschnitt „Threads and QObject“ gibt es ein Absatz über Signals and Slots across Threads, dort sind die Hintergründe erklärt.
Shaldy
User
Beiträge: 123
Registriert: Sonntag 2. März 2008, 22:49

Okay, danke schonmal für die Antwort!
Jetzt ist das Problem, dass ich mit der Entwicklung unter extremem Zeitdruck stehe (ja ich weiß, selber schuld, wenn ich mich vorher nicht genug informiere), aber ich muss das Problem (welches jetzt übrigens seit ca 50 Versuchen nicht mehr aufgetreten ist) nunmal in den Griff kriegen.
Soweit ich das jetzt sehe liegt es daran, dass ich aus dem 2. Thread heraus eine Funktion aufrufe, welche ein QListWidget sortiert (QObject::connect: Cannot queue arguments of type 'QItemSelection'). Komm ich da irgendwie drum herum?

Werde mich danach auch umgehend mit der Dokumentation beschäftigen (versprochen)!
Dies ist keine Signatur!
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Shaldy hat geschrieben: Soweit ich das jetzt sehe liegt es daran, dass ich aus dem 2. Thread heraus eine Funktion aufrufe, welche ein QListWidget sortiert (QObject::connect: Cannot queue arguments of type 'QItemSelection').
Auf UI-Elemente solltest Du aus einem Thread niemals direkt zugreifen. Ein QListWidget ist ja nicht modellbasiert, insofern könnte das problematisch sein. Vermutlich musst Du eben über eigene Signale und Slots die Sortierung in einem Model vornehmen. Dazu bietet sich es dann eben auch an, ein modellbasiertes Widget wie ein QListView zur Anzeige zu nehmen.

Aber ohne ein minimales lauffähiges Beispiel kann man hier nur rumraten.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Shaldy
User
Beiträge: 123
Registriert: Sonntag 2. März 2008, 22:49

Was meinst du mit eigenen Signalen und Slots, also in wiefern komme ich damit ums Problem herum und warum genau kommt mein Fehler nun eigentlich?
Bin ziemlich ratlos...

EDIT: Okay, bin jetzt über die Klasse QMutex gestolpert, welche, soweit ich das sehe, den lock-objekten im thread Modul recht nahe kommen. Der Dokumentation habe ich entnommen, dass ich, sobald ich auf Daten außerhalb eines Threads zugreife, ich diese mit einem solchen QMutex schützen muss. Heißt das, dass damit das Problem gelöst ist?

EDIT2: Bin mit QMutex nicht sehr viel weitergekommen. Da in der Fehlermeldung der Hinweis steht, ich solle prüfen, ob QItemSelection ein registrierter Metatyp ist, habe ich vergeblich nach der PyQt Implementierung von qRegisterMetaType() gesucht, aber nirgendwo gefunden. Google hat mir da genau so wenig geholfen.
Also, wie kann ich eine Klasse als Metatypen registrieren?
Dies ist keine Signatur!
lunar

@Shaldy: "qRegisterMetaType()" ist eine Template-Funktion, die sich nicht in Python einbinden lässt. Man kann in PyQt keine „Metatypen“ registrieren.

"QMutex" ist im Übrigen sicherlich auch nicht die Lösung, denn mit dem Problem hat das überhaupt nichts zu tun. Was die Lösung ist, kann man Dir nicht sagen, da Du nach wie vor kein Beispiel gezeigt hast, bei dem dieser Fehler auftritt.
Shaldy
User
Beiträge: 123
Registriert: Sonntag 2. März 2008, 22:49

Also, ich habe den Fehler mit print Anweisungen auf eine bestimmte Zeile eingegrenzt. Ich rufe aus dem 2. Thread eine Funktion im ersten Thread auf, die unter anderem die Funktion self.qwidgetlist.clear() ausführt. Dort wird auch die Fehlermeldung ausgegeben.
Seit dem ich aber die print Anweisungen eingefügt habe, kommt der Fehler garnicht mehr!!!

Hier mal noch der Code:

1. Thread:

Code: Alles auswählen

class Foo(QMainWindow):
    def __init__(self, panret = None):
        ...
        self.update_check = UpdateCheck() #<--------- Der Thread. Code weiter unten
        self.update_check.start()
    ...
    def getNotifications(self):
        if not self.connected:
            title = "Fehler"
            msg = "Es besteht keine Verbindung zum Server."
            return self.critical(title, msg)
        if not self.secure_mode:
            title = "Vollzugriff ist nicht aktiviert"
            msg = "Um die Webservices zu erhalten, müssen Sie " + \
                      "den Vollzugriff aktivieren."
            return self.information(title, msg)
        #Read private id an convert to string
        private_id = self.sc_connection.read(80, 28)
        private_id = reduce(lambda x, y: str(x) + str(y), private_id)
        print "haltepunkt 1"
        #Try to connect server
        try:
            self.notifications = self.connection.getNotifications(private_id)
        except ePassportError, e:
            return self.critical("Fehler", str(e))
        #Connection succesfull
        #clear list
        print "haltepunkt 2"
        self.ui.lst_messages.clear() #<------------------- Hier kam immer der Fehler!
        #insert messages in list
        unread = filter(lambda x: x["read"] == "0", self.notifications)
        name_list = [n["webserviceName"] for n in unread]
        accepted_webservices = filter(lambda x: x["status"] == "1", self.webservices)
        for s in accepted_webservices:
            search = s["name"]
            number = name_list.count(search)
            if number == 0:
                item = QListWidgetItem(search)
            else:
                item = QListWidgetItem(search + " (" + str(number) + ")")
            self.ui.lst_messages.addItem(item)
        self.ui.lst_messages.sortItems()
        #set tab text
        text = "Nachrichten ("
        #count unread notifications from accepted webservices
        accepted_webservices_names = [x["name"] for x in accepted_webservices]
        unread_accepted = filter(
            lambda x: x["webserviceName"] in accepted_webservices_names,
            unread)
        if len(unread_accepted) != 0:
            text += str(len(unread_accepted))
            text += ")"
            self.ui.tbw_menu.setTabText(3, text)
        else: #no unread notification
            self.ui.tbw_menu.setTabText(3, "Nachrichten")
UpdateCheck Code:

Code: Alles auswählen

class UpdateCheck(QThread):

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

    def run(self):
        while True:
            sleep(5)
            #Check for new webservices
            #update webservices
            old_webservices = self.parent().webservices
            #get webservices via soap connection because else
            #webservice lists had to be cleared -> bad effect
            #Read private id and convert to string
            private_id = self.parent().sc_connection.read(80, 28)
            private_id = reduce(lambda x, y: str(x) + str(y), private_id)
            new_webservices = self.parent().connection.getWebservices(private_id)
            #filter unregistered webservices
            unreg1 = filter(lambda x: x["status"] == "0", old_webservices)
            unreg2 = filter(lambda x: x["status"] == "0", new_webservices)
            #clear message
            msg = ""
            #compare
            if len(unreg2) > len(unreg1): #new unregistered webservices
                self.parent().getWebservices()
                number = len(unreg2) - len(unreg1)
                msg += str(number) + " neue Anfragen.\n"
            #Check for new notifications
            #update notifications
            old_notifications = self.parent().notifications
            #get notifications via soap connection because else
            #notifications list had to be cleared -> bad effect
            new_notifications = self.parent().connection.getNotifications(private_id)
            #filter notifications from accepted webservices
            accepted_webservices = filter(
                lambda x: x["status"] == "1",
                self.parent().webservices)
            accepted_webservices = [x["name"] for x in accepted_webservices]
            old_notifications = filter(
                lambda x: x["webserviceName"] in accepted_webservices,
                old_notifications)
            new_notifications = filter(
                lambda x: x["webserviceName"] in accepted_webservices,
                new_notifications)
            #filter unread notifications
            unread1 = filter(lambda x: x["read"] == "0", old_notifications)
            unread2 = filter(lambda x: x["read"] == "0", new_notifications)
            #compare
            if len(unread2) > len(unread1): #new unread notification
                print "neue nachricht"
                self.parent().getNotifications() #<--------------- Hier die Funktion, in der der Fehler ausgegeben wird!
                print "NACH FEHLER"
                number = len(unread2) - len(unread1)
                msg += str(number) + " neue Nachrichten."
                #update inbox
                try: #if inbox isn't opened, AttributeError will be raised
                    print "update inbox"
                    self.parent().inbox.update()
                except AttributeError:
                    pass
            if msg != "":
                self.parent().systemtray.showMessage(
                    "Neue Anfragen/Nachrichten",
                msg)
Die Ausgabe sah so aus:
...
haltepunkt 1
haltepunkt 2
QtCore::connect.... (Fehlermeldung)
NACH FEHLER
Dies ist keine Signatur!
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Naja das sieht mir doch eindeutig aus: Du greifst aus einem Thread auf eine GUI-Klasse direkt zu! Das sollte man nicht tun ;-)

Schau Dir mal dieses Beispiel von lunar an: https://bitbucket.org/lunar/snippets/sr ... rogress.py

Dort kannst Du nachvollziehen, wie man so ein Problem löst.

Prinzipiell musst Du in Deiner Thread Klasse ein Signal erstellen, welches Du anstelle des getNotifications()-Aufruds emittest. Dieses Signal musst Du in der Klasse Foo() mit der getNotifications-Methode connecten. Ggf. benötigst Du auch mehrere Signale oder aber Parameter; dass musst Du dann eben noch sehen.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@Shaldy: Wobei das IMHO nicht nur die Stelle betrifft wo Du den Fehler bekommst, denn die ganze `run()`-Methode ist ja *voll* von Zugriffen auf `parent()` und den Methoden dieses Objekts. Das dürfte potentiell alles für lustiges Verhalten bis hin zu Abstürzen verantwortlich sein.
Shaldy
User
Beiträge: 123
Registriert: Sonntag 2. März 2008, 22:49

Danke, Danke, Danke, es funktioniert! :D

@BlackJack: Die anderen Zugriffe sind aber keine, auf GUI Elemente und geschrieben wird ja auch nie, insorfern sehe ich da keine Gefahr (oder doch?)
Dies ist keine Signatur!
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Shaldy hat geschrieben: @BlackJack: Die anderen Zugriffe sind aber keine, auf GUI Elemente und geschrieben wird ja auch nie, insorfern sehe ich da keine Gefahr (oder doch?)
Sicher?

Code: Alles auswählen

self.parent().inbox.update()
# oder auch
self.parent().systemtray.showMessage(
                    "Neue Anfragen/Nachrichten",
                msg)
Sieht für mich schon danach aus, bei letzterem ist es sogar sicher.

Ich persönlich würde das noch stärker trennen! Du musst ja auch nicht alles in eine Funktio klatschen, sondern kannst ja durchaus verschiedene Signale emitten und diese dann im Hauptfenster getrennt connecten. Ich denke dadurch ließe sich auch die Übersichtlichkeit erhöhen. So sieht der Code an vielen Stellen ziemlich wirr aus.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
lunar

@shaldy: Selbst lesender Zugriff auf Steuerelemente ist nur und ausschließlich aus dem Hauptthread heraus erlaubt.
Shaldy
User
Beiträge: 123
Registriert: Sonntag 2. März 2008, 22:49

Aber lesen von einfachen Python Typen wie Listen, etc. ist erlaubt, oder?
Dies ist keine Signatur!
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Shaldy hat geschrieben:Aber lesen von einfachen Python Typen wie Listen, etc. ist erlaubt, oder?
Sofern diese Thread-safe sind sollte das wohl gehen. Ich bin da nicht der Experre auf dem Gebiet, daher warte mal lieber lunars oder BlackJacks "ok" ab :-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
BlackJack

@Hyperion: Ich würde grundsätzlich mal Nein sagen -- nicht ohne explizites Locking. Das "Problem" ist, dass man in CPython sehr viel Unsauberes machen kann, weil es das "global interpreter lock" (GIL) gibt. Das wird aber von der Sprache nicht garantiert -- wenn man Code schreibt, der sich darauf verlässt, kann man also unter anderen Python-Implementierungen böse auf die Nase fallen. Oder auch in zukünftigen CPython-Implementierungen, denn der Wille das GIL loszuwerden oder zumindest zu "lockern" ist ja vorhanden.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

BlackJack hat geschrieben:@Hyperion: Ich würde grundsätzlich mal Nein sagen -- nicht ohne explizites Locking. Das "Problem" ist, dass man in CPython sehr viel Unsauberes machen kann, weil es das "global interpreter lock" (GIL) gibt. Das wird aber von der Sprache nicht garantiert -- wenn man Code schreibt, der sich darauf verlässt, kann man also unter anderen Python-Implementierungen böse auf die Nase fallen. Oder auch in zukünftigen CPython-Implementierungen, denn der Wille das GIL loszuwerden oder zumindest zu "lockern" ist ja vorhanden.
Ah, danke für die Info! Ich habe mich bisher wirklich nur PyQt mit Threading befasst und da die Kommunikation immer über Signals und Slots gelöst mit Qt-Typen.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Shaldy
User
Beiträge: 123
Registriert: Sonntag 2. März 2008, 22:49

Also selbst hier

Code: Alles auswählen

from PyQt4.QtGui import *
from PyQt4.QtCore import QThread
from time import sleep

class Window(QWidget):
    def __init__(self, parent = None):
        QWidget.__init__(self, parent)
        self. liste = [1, 2, 3, 4, 5]

class Thread(QThread):
    def __init__(self, parent = None):
        QThread.__init__(self, parent)
    def run(self):
        while True:
            sleep(5)
            print self.parent().liste

if __name__ == "__main__":
    app = QApplication([])
    w = Window()
    t = Thread(w)
    t.start()
    app.exec_()
Ist schon nicht mehr sicher? Wie kann ich dieses Problem denn dann umgehen?
Dies ist keine Signatur!
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Shaldy hat geschrieben: Ist schon nicht mehr sicher? Wie kann ich dieses Problem denn dann umgehen?
Indem Du eben ein Signal emitierst, welches einen Parameter beinhaltet (oder ggf. mehere), nämlich genau die Daten, die Du an Parent-widget übermitteln willst.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Antworten