Threads: Arbeiter und Schlangen

Viele APIs stehen uns nur in synchroner Form zur Verfügung. Dazu zählen vor allem Funktionen für Kompression und Verschlüsselung.
Will man diese im Hintergrund werken lassen während sich das Hauptprogramm etwa um die UI kümmert, erzeugt man einen weiteren Thread und lässt die Arbeit dort ausführen (Workerthread).

Und dann gibt es APIs die den Kontext des Threads auf dem sie laufen verändern. Es gibt thread-lokale Variablen oder es werden sogar eigene Einsprungpunkte in diesem Thread aktiv.

Das kann wiederum mit anderen APIs kollidieren oder dort Seiteneffekte auslösen. Auch hier greift man gerne zu separaten Threads, wo Aufgaben in eine Warteschlange (Queue) eingefügt werden und dann hintereinander abgearbeitet werden.


Das Component Object Model ist unter Windows so ein Sonderfall, genau so wie die dotNet-Runtime oder die Windows-Runtime (WinRT).

Hier wird der Thread mit bestimmten Parametern gezielt “initialisiert” und muss vor dessen Beendigung auch wieder “de-initialisiert” werden.

Dieser Umstand hat mich schon seit langem dazu bewogen, COM Code immer in separaten Threads auszuführen.

So wurde die “Thread-Execution-Queue” geboren, die ich in fast allen Projekten, an denen ich arbeite, immer einbaue.

Technisch gesprochen handelt es sich dabei pro Instanz um einen separaten Thread, und jeglicher Runtime-abhängiger Code wird dabei gekapselt. Anstatt eine Funktion direkt auszuführen, wird eine Proxy-Funktion bereitgestellt, die ein Thread-Queue-Item anlegt und es der Execution-Queue übergibt. Die Proxy-Funktion wartet dann so lange (z.B. per Sync-Event), bis der eigentlich Code im Execution-Thread fertig ist und kann dann sogar die Ergebnisse oder abgefangene Exceptions dem eigentlich Aufrufer zurückmelden.

Diese Prozedur kann übrigens auch dafür genutzt werden, dass thread-unsicherer Code thread-sicher wird, denn in der Execution-Queue kann immer nur ein Auftrag nach dem FIFO Prinzip abgearbeitet werden.

Aufwändig ist leider das manuelle Schreiben der Proxy-Funktionen. Doch nachdem innerhalb einer API-Familie oft Funktionen ähnlich gestaltet sind, kann man mit Macros und Templates in C oder C++ viel Arbeitsaufwand reduzieren.

Kritisch sind vor allem Rückrufprozeduren, die man entweder in den Aufrufer-Thread zurückleiten muss, oder wo man abwägen muss, ob man direkt im Execution-Queue Kontext weiteren Code ausführt.

Daraus ergibt sich nämlich schnell das Problem, dass ein Rückruf zu einem erneuten Aufruf von Code in der Execution-Queue führt und dieser muss dann ohne Umleitungen ausgeführt werden, weil die Queue ja bereits arbeitet und nicht auf sich selbst warten darf.

Aus diesem Grund befindet sich im GATE Projekt in einigen Modulen Code, der anders reagiert, wenn der aktuelle Thread-Context einem zwischengespeicherten Kontext entspricht, was auf einen Rückruf aus einem Execution-Queue-Thread hindeutet.

Neuere Programmiersprachen und Frameworks nutzen asynchronen Code häufiger und nutzen ihre eigenen State-Machines um Code automatisch in anderen Threads auszuführen.
Dabei sollte man aber nicht unterschätzen, dass es auch hier zu Seiteneffekten kommen kann.

Fazit

Ich empfehle daher mir selbst und jedem anderen Bibliotheksentwickler bei solchen Thread-Wechsel-Aktionen wie bei asynchroner Programmierung, oder kontextabhängiger Ausführung sich Gedanken über alle möglichen Zustände der eigenen Variablen aber auch der Umgebung zu machen.

Nichts ist lästiger als ein Modul, dass “normalerweise” immer funktioniert, und dann ausgerechnet in kritischen Fällen schwer versagt, weil es mit weiteren Komponenten zu Konflikten kam.

Oft musste ich schon recht komplizierte Execution-Queues um Fremdbibliotheken spinnen, weil sie seltsame Dinge mit globalen Variablen oder thread-lokalen Variablen anstellten, ohne dass dies dokumentiert war.