F
@Enumerator sagte in Multithreading und Class Inheritance:
Nein, in 99% der Zeit passiert im Grunde nichts, wenn nicht z. B. irgendwelche Animationen laufen. Aber ein Szenario wo ich im Moment noch etwas Performance Probleme habe ist z. B. wenn das Fenster resized wird. Meine Idee war dass dann ein Control mit Child Controls anstatt direkt "Arrange" mit der neuen Größe aufzurufen dies für jedes Child als "Task" z. B. in eine TaskQueue ablegen könnte welche dann von einem ThreadPool abgearbeitet wird.
Nur damit ich das richtig verstehe: Meine Interpretation dieser knappen Bescheibung ist, dass Kind-UI-Elemente Positionen und Größen relativ zu ihrem Eltern-Element haben. Die Arrange-Operation berechnet dann rekursiv die aktualisierten absoluten Koordinaten und speichert sie ebenfalls im UI-Baum. Ist das so korrekt?
Bevor ich versuchen würde, das Problem mit mehreren Threads zu erschlagen, würde ich allderings sicherstellen wollen, dass mein Problem nicht irgendwo anders liegt, und mir folgende Fragen stellen:
Wo hakt es denn da genau beim Resize? Kann einen ein Profiler da auf die richtige Spur bringen, oder ist "alles irgendwie langsam"?
Wieviele Elemente müssen da angefasst werden? Muss jedes Element nur einmal durchlaufen werden oder hast du da irgendwelchen Code drin, der die Komplextät mit der Anzahl der Elemente rasant anwachsen lässt? Können bessere Algorithmen oder Datenstrukturen vielleicht helfen?
Machst du vielleicht irgendwo mehr Arbeit als notwendig - z.B. braucht es nur einen rekursiven Arrange-Aufruf, wenn sich tatsächlich ein relevanter Parameter eines Eltern-Elemenst verändert hat. Auch sollte man drauf achten, dass man keine "kreuz- und quer-Rekursion" drin hat, wenn z.B. eine Größenänderung eines Kindelements wieder Einfluss auf die Größe des Elternelements haben kann.
Allerdings stellt sich die Frage ob der ganze Task-Overhead dann nicht wieder alles zunichte macht. Andererseits ist da auch noch einiges Optimierungspotenzial. Z.B. sollen die Menuitems später in einem eigenen (Child-)Window gerendert werden was dann die Arrange Kette durchbrechen würde. Und alleine ein umfangreiches Menü sind schnell je nach Style hunderte einzel Objekte.
Wird schon seinen Grund haben das Microsoft in C#/WPF auch nur einen UI Thread hat. Evtl. macht es wirklich mehr Sinn dann mit "Wokerthreads" z.B. für Animationen zu arbeiten anstatt die ganze UI multithreaded zu gestalten.
Für simple UIs ist ein zentraler Baum sicher völlig in Ordnung. Worte wie "Animation" und "Rendering" lassen mich jedoch vermuten, dass du da etwas mehr mit vorhast. Damit kann man schon an Grenzen stossen - besonders wenn da tatsächlich User-, Layouting- und Rendering-Threads mit vielleicht >= 60fps durch diese eine Datenstruktur durchballern sollen.
Eventuell macht es Sinn, nicht alle Eigenschaften des UI in einer einzigen Datenstruktur zu speichern, sondern mehrere Repräsentationen anzulegen, die auf den jeweiligen Anwendungsfall angepasst sind.
So könnte es z.B. eine Baumstruktur für die user-seitige API geben und flache Array-Datenstrukturen für "Arrange"/Layoutberechnungen und für das letztendliche Rendering. Manchmal kann es nämlich tatsächlich performancetechnisch Sinn machen, mit zusätzlichem Aufwand Daten zu kopieren und sogar redundant vorzuhalten, wenn man sie damit in eine Datenstruktur bringt, die günstiger für einen Algorithmus oder die Arbeitsweise moderner CPUs ist (Cache und Branch Prediction als Stichworte).
Solche separaten Datenstrukturen haben dann auch noch den Vorteil, dass man die Arbeit darauf leichter auf mehrere Threads aufsplitten kann, die dann mehr oder weniger ungestört auf ihren eigenen Datenstrukturen arbeiten können (mit nur wenigen Synchronisationspunkten).
In diesem Kontext ist vielleicht auch ein Vortrag von der Cppcon 2018 über DatA-Oriented Design für dich interessant, vor allem weil es hier sehr konkret um UI geht. Hier auf dem "fancyness"-Niveau einer Browser-Engine - in diesem Fall für UIs innerhalb von Spielen:
https://www.youtube.com/watch?v=yy8jQgmhbAU
Der Vortrag geht nicht bis ins letzte Implementierungs-Detail, aber vielleicht dann man sich da trotzdem ein paar Ideen abholen.
@Finnegan sagte in Multithreading und Class Inheritance:
In so einem Fall könnte es helfen, z.B. UI und Code, der auf UI-Events reagiert in separaten Threads voneinander zu entkoppeln. Kommunikation z.B. über eine synchronisierte Message-Queue.
Das hört sich interessant an. Evtl. kannst du nochmal ein kurzes Pseudocode Beispiel geben wie du das mit der synchronisierten MessageQueue meinst?
Nichts furchtbar schlaues. Im einfachsten Fall reicht ein simpler std::queue der über einen einzigen Mutex synchronisiert wird. Die Idee ist, dass User-Code, der auf ein UI-Ereignis wie z.B. einen Knopfdruck reagiert, nicht im UI-Thread ausgeführt wird und diesen blockiert, während der User-Code z.B. gerade ein paar GiB Daten kopiert.
Man hat in UI-Code ja öfter solche oder ähnliche Muster hier:
// User-Thread
user_code(Button button)
{
// viel zu tun!
...
}
...
button.registerEventHandler(BUTTON_PRESS, user_code);
// UI-Thread (oder ebenfalls User-Thread, wenn Single-Threaded)
button::on_press()
{
foreach (handler : button_press_handlers)
handler(this); // ruft user_code() auf und hat viel zu tun -> UI Thread blockiert.
}
Die Idee ist nun, den User Code nicht im UI-Thread aufzurufen, sondern wieder im User-Thread, so dass der UI-Thread unbehelligt weiterarbeiten kann und seine "Schwuppdizität" nicht einbüsst (sehr grober Pseudocode, ohne Mutex-Synchronisation, etc):
// UI-Thread
button::on_press()
{
for (handler : button_press_handlers)
message_queue.push_back(ButtonPressEvent(this, handler));
}
// User-Thread:
process_events()
{
while (!message_queue.empty())
{
event = message_queue.pop_front();
event.handler(event.object);
}
}
...
button.registerEventHandler(BUTTON_PRESS, user_code);
...
process_events();