Richtiges Einfädeln in Qt

Qt ist ein äußerst leistungsfähiges und praktisches Framework für C ++. Diese Bequemlichkeit hat jedoch einen Nachteil: Viele Dinge in Qt passieren vor dem Benutzer verborgen. In den meisten Fällen funktioniert die entsprechende Funktionalität in Qt „magisch“ und lehrt den Benutzer, diese Magie einfach als selbstverständlich zu betrachten. Wenn die Magie dennoch bricht, ist es äußerst schwierig, ein Problem zu erkennen und zu lösen, das plötzlich auf einer Ebene erscheint, die scheinbar flach ist.

Dieser Artikel ist ein Versuch zu systematisieren, wie Qt "unter der Haube" die Arbeit mit Flows implementiert, und über eine Reihe nicht offensichtlicher Fallstricke, die mit den Einschränkungen dieses Modells verbunden sind.

Die Grundlagen
Thread-Affinität, Initialisierung und ihre Einschränkungen
Hauptthread, QCoreApplication und GUI
Thread rendern
Fazit

Die Grundlagen


Beginnen wir mit den Grundlagen. In Qt sind alle Objekte, die Signale und Slots verarbeiten können, Nachkommen der QObject-Klasse. Diese Objekte sind von Natur aus nicht kopierbar und stellen logischerweise einige einzelne Entitäten dar, die miteinander „sprechen“ - auf bestimmte Ereignisse reagieren und selbst Ereignisse erzeugen können. Mit anderen Worten, QObject in Qt implementiert das Actors-Muster . Bei korrekter Implementierung ist jedes Qt-Programm im Wesentlichen nichts anderes als ein Netzwerk von QObjects, die miteinander interagieren und in denen die gesamte Programmlogik „lebt“.

Zusätzlich zu einer Reihe von QObjects kann ein Qt-Programm Datenobjekte enthalten. Diese Objekte können keine Signale erzeugen und empfangen, sondern können kopiert werden. Beispielsweise können Sie QStringList und QStringListModel untereinander vergleichen. Eines davon ist QObject und kann nicht kopiert werden, kann jedoch direkt mit UI-Objekten interagieren. Das andere ist ein regulärer kopierbarer Container für Daten. Objekte mit Daten werden wiederum in „Qt-Metatypen“ und alle anderen unterteilt. Beispielsweise ist QStringList ein Qt-Metatyp, std :: list <std :: string> (ohne zusätzliche Gesten) jedoch nicht. Ersteres kann in jedem Qt-shnom-Kontext verwendet werden (über Signale übertragen, in QVariant liegend usw.), erfordert jedoch ein spezielles Registrierungsverfahren, und die Klasse muss über einen öffentlichen Destruktor, einen Kopierkonstruktor und einen Standardkonstruktor verfügen. Die zweiten sind beliebige C ++ - Typen.

Gehen Sie nahtlos zu den eigentlichen Themen über


Wir haben also bedingte „Daten“ und es gibt einen bedingten „Code“, der mit ihnen funktioniert. Aber wer wird diesen Code tatsächlich ausführen? Im Qt-Modell wird die Antwort auf diese Frage explizit festgelegt: Jedes QObject ist streng an einen QThread-Thread gebunden, der tatsächlich Slots und andere Ereignisse dieses Objekts bedient. Ein Thread kann viele QObjects gleichzeitig oder gar keine bedienen, aber QObject hat immer einen übergeordneten Thread und es ist immer genau einer. Tatsächlich können wir davon ausgehen, dass jeder QThread einen Satz von QObject "besitzt". In der Qt-Terminologie wird dies als Thread-Affinität bezeichnet. Versuchen wir zur Verdeutlichung zu visualisieren:



In jedem QThread befindet sich eine Warteschlange mit Nachrichten, die an Objekte adressiert sind, die dieser QThread „besitzt“. Im Qt-Modell wird davon ausgegangen, dass wir, wenn ein QObject eine Aktion ausführen soll, eine QEvent-Nachricht an dieses QObject "senden":

QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority); 

Bei diesem thread-sicheren Aufruf findet Qt den QThread, zu dem das Empfängerobjekt gehört, schreibt das QEvent in die Nachrichtenwarteschlange dieses Threads und weckt diesen Thread bei Bedarf. Es wird erwartet, dass der Code, der irgendwann danach in diesem QThread ausgeführt wird, die Nachricht aus der Warteschlange liest und die entsprechende Aktion ausführt. Damit dies wirklich geschieht, muss der Code in QThread in die QEventLoop-Ereignisschleife eintreten, das entsprechende Objekt erstellen und entweder die exec () -Methode oder die processEvents () -Methode aufrufen. Die erste Option tritt in eine endlose Nachrichtenverarbeitungsschleife ein (bevor der QEventLoop das Ereignis quit () empfängt), die zweite beschränkt sich auf die Verarbeitung von Nachrichten, die sich zuvor in der Warteschlange angesammelt haben.



Es ist leicht zu erkennen, dass Ereignisse für alle Objekte, die zu einem Thread gehören, nacheinander verarbeitet werden. Wenn die Verarbeitung eines Ereignisses durch einen Thread lange dauert, werden alle anderen Objekte "eingefroren" - ihre Ereignisse sammeln sich in der Warteschlange des Streams an, werden jedoch nicht verarbeitet. Um dies zu verhindern, bietet Qt die Möglichkeit des kooperativen Multitasking - Event-Handler können überall „vorübergehend unterbrechen“, indem sie eine neue QEventLoop erstellen und die Steuerung an diese übergeben. Da der Ereignishandler zuvor auch von QEventLoop im Stream aufgerufen wurde, wird bei diesem Ansatz eine Kette von ineinander verschachtelten Ereignisschleifen gebildet.

Ein paar Worte zum Event Dispatcher
Genau genommen ist QEventLoop nichts anderes als ein benutzerfreundlicher Wrapper über ein systemabhängiges Grundelement auf niedrigerer Ebene namens Event Dispatcher und implementiert die QAbstractEventDispatcher-Schnittstelle. Er ist es, der die eigentliche Erfassung und Verarbeitung von Ereignissen durchführt. Ein Thread kann nur einen QAbstractEventDispatcher haben und wird nur einmal installiert. Ab Qt5 können Sie so den Dispatcher bei Bedarf einfach durch einen geeigneteren ersetzen, indem Sie der Initialisierung des Streams nur eine Zeile hinzufügen und ohne die potenziell zahlreichen Stellen zu berühren, an denen QEventLoop verwendet wird.

Was beinhaltet das Konzept eines „Ereignisses“, das in einem solchen Zyklus verarbeitet wird? Allen Qt-Mitarbeitern bekannt, ist „Signale“ nur ein besonderes Beispiel, QEvent :: MetaCall. Ein solches QEvent speichert einen Zeiger auf die Informationen, die erforderlich sind, um die aufzurufende Funktion (Slot) und ihre Argumente zu identifizieren. Zusätzlich zu den Signalen in Qt gibt es jedoch ungefähr hundert (!) Andere Ereignisse, von denen ein Dutzend für spezielle Qt-Ereignisse (ChildAdded, DeferredDelete, ParentChange) reserviert ist und der Rest verschiedenen Nachrichten des Betriebssystems entspricht.

Warum gibt es so viele von ihnen und warum war es unmöglich, nur auf Signale zu verzichten?
Der Leser mag fragen: Warum gibt es so viele Ereignisse und warum war es unmöglich, mit nur einem praktischen und universellen Signalmechanismus auszukommen? Tatsache ist, dass unterschiedliche Signale sehr unterschiedlich verarbeitet werden können. Beispielsweise sind einige der Signale komprimierbar. Wenn die Warteschlange bereits eine Rohnachricht dieses Typs enthält (z. B. QEvent :: Paint), ändern die nachfolgenden Nachrichten diese einfach. Andere Signale können herausgefiltert werden. Das Vorhandensein einer kleinen Anzahl von Standard- und leicht identifizierbaren QEvents vereinfacht die entsprechende Verarbeitung erheblich. Darüber hinaus wird die QEvent-Verarbeitung aufgrund eines deutlich einfacheren Geräts normalerweise etwas schneller ausgeführt als die Verarbeitung eines ähnlichen Signals.

Eine der nicht offensichtlichen Fallstricke hier ist, dass in Qt ein Stream im Allgemeinen möglicherweise nicht einmal einen Dispatcher und daher keinen einzigen EventLoop hat. Objekte, die zu diesem Stream gehören, reagieren nicht auf an sie gesendete Ereignisse. Da QThread :: run () standardmäßig QThread :: exec () aufruft, in dem die Standard-EventLoop gerade implementiert ist, sind diejenigen, die häufig versuchen, ihre eigene Version von run () zu ermitteln, die von QThread erbt, häufig mit diesem Problem konfrontiert. Ein ähnlicher Anwendungsfall für QThread ist im Prinzip durchaus gültig und wird sogar in der Dokumentation empfohlen, widerspricht jedoch der oben beschriebenen allgemeinen Idee, den Code in Qt zu organisieren, und funktioniert häufig nicht so, wie Benutzer es erwarten . Ein typischer Fehler in diesem Fall ist der Versuch, einen solchen benutzerdefinierten QThread durch Aufrufen von QThread :: exit () oder quit () zu stoppen. Beide Funktionen senden eine Nachricht an QEventLoop, aber wenn sich einfach kein QEventLoop im Stream befindet, gibt es natürlich niemanden, der sie verarbeitet. Infolgedessen versuchen unerfahrene Benutzer, die versuchen, "eine defekte Klasse zu reparieren", ein "funktionierendes" QThread :: terminate zu verwenden, was absolut unmöglich ist. Denken Sie daran: Wenn Sie run () neu definieren und die Standardereignisschleife nicht verwenden, müssen Sie einen Mechanismus zum Beenden des Threads selbst bereitstellen - beispielsweise mithilfe der speziell hinzugefügten Funktion QThread :: requestInterruption (). Es ist jedoch richtiger, einfach nicht von QThread zu erben, wenn Sie nicht wirklich eine spezielle neue Art von Threads implementieren und entweder den speziell für solche Skripte erstellten QtConcurrent verwenden oder die Logik in ein spezielles Worker-Objekt einfügen, das von QObject geerbt wurde, und dieses in Standard-QThread einfügen und verwalten Arbeiter mit Signalen.

Thread-Affinität, Initialisierung und ihre Einschränkungen


Wie wir bereits herausgefunden haben, "gehört" jedes Objekt in Qt zu einem Stream. Gleichzeitig stellt sich eine logische Frage: Zu welcher eigentlich genau? Die folgenden Konventionen werden in Qt akzeptiert:

1. Alle „Kinder“ eines „Elternteils“ leben immer im selben Stream wie der Elternteil

Dies ist möglicherweise die stärkste Einschränkung des Qt-Stream-Modells, und Versuche, es zu brechen, führen für den Benutzer häufig zu sehr seltsamen Ergebnissen. Beispielsweise schlägt ein Versuch, setParent für ein Objekt zu erstellen, das in einem anderen Thread in Qt lebt, einfach stillschweigend fehl (eine Warnung wird in die Konsole geschrieben). Anscheinend wurde dieser Kompromiss aufgrund der Tatsache erzielt, dass die fadensichere Entfernung von „Kindern“ im Falle des Todes eines Elternteils, der in einem anderen Faden lebt, sehr trivial und anfällig für schwer zu fassende Fehler ist. Wenn Sie eine Hierarchie interagierender Objekte implementieren möchten, die in verschiedenen Streams leben, müssen Sie das Löschen selbst organisieren.

2. Ein Objekt, dessen übergeordnetes Element während der Erstellung nicht angegeben wurde, befindet sich in dem Stream, der es erstellt hat

Alles hier zur gleichen Zeit und einfach und zur gleichen Zeit ist nicht immer offensichtlich. Aufgrund dieser Regel befindet sich QThread (als Objekt) beispielsweise in einem anderen Thread als der Thread, den es selbst steuert (und gemäß Regel 1 kann es keines der in diesem Thread erstellten Objekte besitzen). Wenn Sie beispielsweise QThread :: run neu definieren und darin QObject-Nachkommen erstellen, reagieren die erstellten Objekte ohne besondere Maßnahmen (wie im vorherigen Kapitel erläutert) nicht auf Signale.

Die Thread-Affinität kann bei Bedarf durch Aufrufen von QObject :: moveToThread geändert werden. Gemäß Regel 1 können nur "Eltern" der obersten Ebene (für die Eltern == null sind) verschoben werden. Ein Versuch, ein "Kind" zu verschieben, wird stillschweigend ignoriert. Wenn sich der übergeordnete „Elternteil“ bewegt, werden auch alle seine „Kinder“ in einen neuen Stream verschoben. Seltsamerweise ist der Aufruf von moveToThread (nullptr) auch legal und bietet die Möglichkeit, ein Objekt mit einer Thread-Affinität von „null“ zu erstellen. Solche Objekte können keine Nachrichten empfangen.

Sie können den "aktuellen" Ausführungsthread durch einen Aufruf der Funktion QThread :: currentThread () abrufen, dem Thread, dem das Objekt zugeordnet ist - durch einen Aufruf von QObject :: thread ().

Eine interessante Frage zur Aufmerksamkeit
Beachten Sie, dass die Implementierung der Funktion des Eigentums an Objekten und der Speicherung von an sie gerichteten QEvents offensichtlich erfordert, dass der Datenfluss die entsprechenden Daten irgendwo speichert. Im Fall von Qt ist die QThread-Basisklasse normalerweise an der Extraktion und Verwaltung solcher Daten beteiligt. Aber was passiert, wenn Sie ein QObject in einem std :: thread erstellen oder die QThread :: currentThread () -Funktion von diesem Thread aus aufrufen? Es stellt sich heraus, dass Qt in diesem Fall implizit „hinter den Kulissen“ ein spezielles Wrapper-Objekt QAdoptedThread erstellt, das kein Eigentümer ist. Gleichzeitig ist es Sache des Benutzers, unabhängig sicherzustellen, dass alle Objekte in einem solchen Stream gelöscht werden, bevor der Stream, der sie generiert hat, gestoppt wird.

Hauptthread, QCoreApplication und GUI


Unter allen Threads wird Qt definitiv einen „Haupt-Thread“ herausgreifen, der im Fall von UI-Anwendungen auch zu einem GUI-Thread wird. In diesem Thread befindet sich das QApplication-Objekt (QCoreApplication / QGuiApplication), das der Hauptereignisschleife dient, die für die Arbeit mit Nachrichten des Betriebssystems ausgerichtet ist. Gemäß Regel Nr. 2 aus dem vorherigen Abschnitt ist in der Praxis der "Haupt" -Thread derjenige, der das QApplication-Objekt tatsächlich erstellt hat. Da in vielen Betriebssystemen der "Hauptthread" eine besondere Bedeutung hat, wird in der Dokumentation dringend empfohlen, QApplication mit dem allerersten Objekt im Ganzen zu erstellen Qt programmieren und sofort nach dem Start der Anwendung ausführen (== innerhalb des ersten Threads im Prozess). Um einen Zeiger auf den Hauptthread der Anwendung zu erhalten, können Sie eine Konstruktion des Formulars QCoreApplication :: instance () -> thread () verwenden. Rein technisch gesehen kann QApplication jedoch auch an einen Nicht-Main () - Stream gehängt werden , wenn beispielsweise die Qt-Schnittstelle in einer Art Plug-In erstellt wird und dies in vielen Fällen problemlos funktioniert.

Aufgrund der Regel "Erstellte Objekte erben den aktuellen Thread" können Sie immer ruhig arbeiten, ohne die Grenzen eines Threads zu überschreiten. Alle erstellten Objekte werden automatisch zur Wartung an den Hauptthread gesendet, wo es immer eine Ereignisschleife gibt und (aufgrund des Fehlens anderer Threads) niemals Probleme mit der Synchronisierung auftreten. Selbst wenn Sie mit einem komplexeren System arbeiten, das Multithreading erfordert, fallen die meisten Objekte höchstwahrscheinlich in den Hauptstrom, mit Ausnahme der wenigen, die explizit an einer anderen Stelle platziert werden. Vielleicht ist es genau dieser Umstand, der zu der scheinbaren „Magie“ führt, in der Objekte ohne Aufwand unabhängig zu arbeiten scheinen (weil kooperatives Multitasking innerhalb des Flusses implementiert ist) und gleichzeitig keine Synchronisation, Blockierung oder dergleichen erfordern (weil alles in einem Thread geschieht) )

Neben der Tatsache, dass der "Haupt" -Thread der "erste" ist und die Hauptverarbeitungsschleife für QCoreApplication-Ereignisse enthält, besteht eine weitere Einschränkung von Qt darin, dass alle mit der GUI verbundenen Objekte in diesem Thread "leben" müssen. Dies ist teilweise eine Folge von Legacy: Aufgrund der Tatsache, dass in einer Reihe von Betriebssystemen alle Operationen mit der GUI nur im Hauptthread ausgeführt werden können, unterteilt Qt alle Objekte in "Widgets" und "Nicht-Widgets". Widget-artige Objekte können nur im Haupt-Thread leben. Ein Versuch, ein solches Objekt in einem anderen zu "überwiegen", wird automatisch ausgelöst. Aufgrund dessen gibt es sogar eine spezielle QObject :: isWidgetType () -Methode, die ziemlich tiefe interne Unterschiede in der Mechanik der Arbeit mit solchen Objekten widerspiegelt. Es ist jedoch interessant, dass in dem viel neueren QtQuick, in dem versucht wurde, mit isWidgetType von der Krücke wegzukommen, das gleiche Problem bestehen blieb

Was ist los? In Qt5 sind QML-Objekte keine Widgets mehr und können in einem separaten Thread gerendert werden. Dies führte jedoch zu einem anderen Problem - Synchronisationsschwierigkeiten. Das Rendern von UI-Objekten ist ein „Lesen“ ihres Status und sollte konsistent sein: Wenn wir versuchen, den Status eines Objekts gleichzeitig mit dem Rendern zu ändern, gefällt uns das Ergebnis der resultierenden „Rasse“ möglicherweise nicht. Darüber hinaus wird OpenGL, um das die „neue“ Grafik Qt erstellt wird, durch die Tatsache, dass die Bildung von Zeichenbefehlen von einem Thread ausgeführt wird, der mit einem globalen Status arbeitet, extrem „geschärft“ - dem „grafischen Kontext“, der sich nur als eine Reihe von aufeinanderfolgenden Operationen ändern kann. Wir können einfach nicht gleichzeitig zwei verschiedene Grafikobjekte auf dem Bildschirm zeichnen - sie werden immer nacheinander gezeichnet. Infolgedessen kehren wir zur gleichen Lösung zurück - das Rendern der Benutzeroberfläche wird einem Thread zugewiesen. Ein aufmerksamer Leser wird jedoch feststellen, dass dieser Thread nicht der Haupt-Thread sein muss - und in Qt5 wird das Framework wirklich versuchen, einen separaten Rendering-Thread dafür zu verwenden

Thread rendern


Im Rahmen des neuen Qt5-Modells erfolgt das gesamte Rendern von Objekten in einem speziell dafür zugewiesenen Thread, dem Rendern von Threads. Gleichzeitig werden die Objekte implizit in ein „Front-End“ unterteilt, das der Programmierer sieht, und normalerweise in ein „Back-End“, das ihm verborgen bleibt und das tatsächlich das eigentliche Rendern ausführt, damit dies sinnvoll ist und nicht nur auf einen einfachen Wechsel von einem „Hauptstrom“ zu einem anderen beschränkt ist. Das hintere Ende befindet sich im Rendering-Thread, während das vordere Ende theoretisch in jedem anderen Thread leben kann. Es wird angenommen, dass das Front-End die nützliche Arbeit (falls vorhanden) in Form einer Ereignisverarbeitung ausführt, während die Back-End-Funktion nur durch Rendern eingeschränkt wird. Theoretisch stellt sich heraus, dass es sich um eine Win-Win-Situation handelt: Die Rückseite „fragt“ regelmäßig den aktuellen Status der Objekte ab und zeichnet sie auf den Bildschirm, während dies nicht durch die Tatsache „gestoppt“ werden kann, dass einige der Objekte während der Verarbeitung des Ereignisses zu viel „nachgedacht“ haben In einem anderen Thread erfolgt eine langsame Verarbeitung. Der Objektfluss muss wiederum nicht auf "Antworten" des Grafiktreibers warten, die den Abschluss des Renderns bestätigen, und verschiedene Objekte können in verschiedenen Flüssen arbeiten.

Aber wie ich bereits im vorigen Kapitel erwähnt habe, müssen wir sie irgendwie synchronisieren, da wir einen Stream haben, der Daten erstellt (eine Vorderseite) und einen Stream, der sie liest (eine Rückseite). Diese Synchronisation in Qt erfolgt durch Sperren. Der Stream, in dem sich die Front befindet, wird vorübergehend angehalten, gefolgt von einem speziellen Funktionsaufruf (QQuickItem :: updatePaintNode (), QQuickFramebufferObject :: Renderer :: synchronize ()), dessen einzige Aufgabe darin besteht, das für die Visualisierung relevante Objekt von vorne nach hinten zu kopieren ". In diesem Fall erfolgt der Aufruf einer solchen Funktion innerhalb des Rendering-Threads . Aufgrund der Tatsache, dass der Thread, in dem sich das Objekt gerade befindet, gestoppt ist, kann der Benutzer mit den Daten des Objekts frei arbeiten, als ob dies „wie gewohnt“ innerhalb des Streams geschehen wäre, zu dem das Objekt gehört.

Ist alles in Ordnung, ist alles in Ordnung? Leider beginnen hier keine und ziemlich unauffälligen Momente. Wenn wir für jedes Objekt einzeln eine Sperre vornehmen, ist dies ziemlich langsam, da der Rendering-Thread warten muss, bis diese Objekte ihre Ereignisse verarbeitet haben. Der "Hang" -Stream, in dem sich das Objekt befindet, ist "Hang" und Rendern. Außerdem wird eine „Desynchronisierung“ möglich, wenn bei gleichzeitiger Änderung von zwei Objekten eines in Bild N und das andere nur in Bild N + 1 gezeichnet wird. Es ist vorzuziehen, die Sperre nur einmal und für alle Objekte gleichzeitig und nur dann zu übernehmen, wenn wir sicher sind, dass diese Sperre erfolgreich sein wird.

Was wurde implementiert, um dieses Problem in Qt zu lösen? Zunächst wurde entschieden, dass alle "grafischen" Objekte eines Fensters in einem Stream leben. Um ein Fenster zu zeichnen und alle darin enthaltenen Objekte zu sperren, reicht es aus, diesen Stream alleine zu stoppen. Zweitens initiiert der Thread mit UI-Objekten die Sperre für die Aktualisierung des Back-End und sendet eine Nachricht an den Rendering-Thread über die Notwendigkeit, sich selbst zu synchronisieren und zu stoppen (QSGThreadedRenderLoop :: polishAndSync, wenn jemand interessiert ist). Dies stellt sicher, dass der Rendering-Thread niemals auf einen Front-End-Stream „wartet“. Wenn es plötzlich "hängt", zeichnet der Rendering-Thread einfach weiter den "alten" Status der Objekte, ohne Nachrichten über die Notwendigkeit einer Aktualisierung zu erhalten. Dies führt zu ziemlich amüsanten Fehlern der Form "Wenn das Rendering aus irgendeinem Grund das Fenster nicht sofort zeichnen kann, friert der Haupt-Thread ein", aber im Allgemeinen ist es ein vernünftiger Kompromiss. Ab QtQuick 2.0 können sogar eine Reihe von "animierten" Objekten im Render-Thread "aufgefüllt" werden, sodass die Animation auch dann weiter funktioniert, wenn der Haupt-Thread "gedacht" ist.



Die praktische Konsequenz dieser Lösung ist jedoch, dass alle UI-Objekte ohnehin im selben Thread leben müssen. Bei alten Widgets im "Haupt" -Thread, bei neuen Qt Quick-Objekten im QQuickWindow-Objekt-Thread, dem sie gehören. Die letzte Regel wird ziemlich elegant geschlagen - um ein QQuickItem zu zeichnen, muss setParent auf das entsprechende QQuickWindow gesetzt werden, wodurch, wie bereits erläutert, sichergestellt wird, dass das Objekt entweder in den entsprechenden Stream verschoben wird oder der Aufruf von setParent fehlschlägt.

Und jetzt leider eine Fliege in der Salbe: Obwohl verschiedene QQuickWindow theoretisch in verschiedenen Streams leben könnten, erfordert dies in der Praxis ein genaues Senden von Nachrichten vom Betriebssystem an sie und in Qt ist es heute nicht implementiert. In Qt 5.13 versucht QCoreApplication beispielsweise, über sendEvent mit QQuickWindow zu kommunizieren, wobei sich der Empfänger und der sendende Teilnehmer im selben Thread befinden müssen (anstelle von postEvent, wodurch die Threads unterschiedlich sein können). Daher funktioniert QQuickWindow in der Praxis nur in einem GUI-Thread ordnungsgemäß. Daher befinden sich alle QtQuick-Objekte an derselben Stelle. Trotz des Vorhandenseins des Rendering-Threads befinden sich fast alle dem Benutzer zur Verfügung stehenden GUI-bezogenen Objekte weiterhin im selben GUI-Thread. Vielleicht ändert sich dies in Qt 6.

Darüber hinaus ist zu beachten, dass das Framework, da Qt auf vielen verschiedenen Plattformen funktioniert (einschließlich solcher, die kein Multithreading unterstützen), eine anständige Anzahl von Fallbacks bietet und in einigen Fällen die Rendering-Thread-Funktionalität tatsächlich von demselben GUI-Thread ausgeführt wird . In diesem Fall befindet sich die gesamte Benutzeroberfläche, einschließlich des Renderns, in einem Thread, und das Synchronisierungsproblem verschwindet automatisch. Ähnlich verhält es sich mit der älteren Widget-basierten Benutzeroberfläche im Qt4-Stil. Qt «» QSG_RENDER_LOOP .

Fazit


Qt — . , , Qt .

;

  • «» , queued signals
  • «» Qt Event Loop exit()
  • Eltern und Nachkommen leben immer im selben Strom. Nur übergeordnete Elemente der obersten Ebene können von Stream zu Stream übertragen werden. Ein Verstoß gegen diese Regel kann zu einem stillen Ausfall der Operation setParent oder moveToThread führen
  • Ein Objekt, dessen übergeordnetes Element nicht angegeben ist, wird zur Eigenschaft des Threads, den dieses Objekt erstellt hat.
  • Alle GUI-Objekte mit Ausnahme des Rendering-Backends müssen im GUI-Stream gespeichert sein
  • Der GUI-Thread ist derjenige, in dem das QApplication-Objekt erstellt wurde

Ich hoffe, dass dies Ihnen hilft, Qt effizienter zu nutzen und keine Fehler im Zusammenhang mit seinem Multithread-Modell zu machen.

Source: https://habr.com/ru/post/de467261/


All Articles