Das Windows-Thread-Context Problem

Die wenigsten C APIs sind von sich aus vollständig thread-sicher (thread-safe). Aber die meisten sind thread-verträglich (thread-aware).

So lange man ein Objekt oder eine Struktur nie von mehreren Threads gleichzeitig parallel benutzt, passiert nichts Schlimmes.

Windows und seine COM-Architektur machen es einem aber etwas schwerer…


Eigentlich bin ich ja ein Windows und vor allem auch ein COM Fanboy. Das Component Object Model ist ein guter Ansatz, um sprachübergreifend Daten und Funktionen auszutauschen und eben auch deshalb basieren viele GATE C Objekte auf einem ähnlichen Modell.

Doch die meisten Windows COM Klassen erwarten, dass sie in einem COM-Apartment erzeugt werden und auch nur aus diesem einen benutzt werden.

Natürlich gibt es auch Klassenfamilien wie z.B. DirectX die vollkommen “free-threaded” implementiert sind, aber die meisten Codes erfordern eben eine Bindung an einen Thread, für den die COM-Runtime initialisiert wurde.

Genau das ist aber in einem Bibliotheken Framework wie beim GATE Projekt ein ungutes Problem. Denn, dass einige Komponenten den “Aufbau des Threads” verändern müssen um überhaupt funktionieren zu können, ist bzw. soll dem Aufrufer nicht bekannt sein.
Hinzu kommt noch, dass eine COM-Initialisierung keine “billige Operation” ist. Es kann schon mal etwas Dauern, bis ein Thread für COM vorbereitet wurde.

Man kann es sich daher auch nicht einfach machen und beim Aufruf einer Funktion mal schnell im Hintergrund COM initialisieren und vor dem return COM wieder zurücksetzen. Das würde erstens viel zu lange dauern und zweitens würden initialisierte COM Objekte und Pointer plötzlich unbrauchbar werden.

Alternativ könnte man COM über alles darüberlegen. Also jeder neue Thread muss ein COM-Thread werden, damit alles darin arbeiten kann. Doch dann ergibt sich das Mehrere-Bibliotheken-Problem: Was ist, wenn wir fremden Code ausführen, der im Hintergrund eigene Threads hochzieht, die nicht an COM gebunden sind, von dort aus Aufrufe in unseren Code stattfinden, wo COM-Kontexte erwartet werden?

Queue als Lösung

Eigentlich habe ich keine “schöne” Lösung für dieses Problem, doch ein möglicher Workaround ist es, den COM-spezifischen Code zu kapseln und immer in einem eigenen Thread auszuführen. Dieser spezielle Thread ist als Queue implementiert, die Ausführungsaufträge (z.B. Funktionszeiger + Parameter) auslesen und durchführen kann.

Erzeugt man also eine Objektinstanz, wird im Hintergrund ein paralleler COM-Thread hochgefahren. Ruft man nun eine Objekt-Methode über eine Wrapperfunktion auf, wandelt diese den Aufruf in einen Queue-Auftrag um und übergibt ihn der COM-Ausführungsschicht und wartet dann so lange, bis die Queue die Fertigstellung der Bearbeitung zurückmeldet.

Der Aufrufer der Objektmethode merkt also gar nicht, dass seine Anforderung umgeleitet und in einem ganz anderen Thread-Kontext bearbeitet wurde. Ebenso erhält der Aufrufer keine nativen COM-Objekt-Pointer zurück sondern Handles oder andere Kapselungsformen.

COM Queue im GATE Framework

Inzwischen habe ich schon ein paar Routinen mit Hilfe von COM Klasse implementiert. Doch nun ist es an der Zeit diese “ordentlich” zu abstrahieren und Kontext-sicher zu gestalten.
COM erhält dafür im Framework eine eigene Plattformerweiterung, wo nur in der Windows Implementierung entsprechende Hilfsfunktionen und Objekte eingearbeitet sind.

Es handelt sich hierbei zwar um öffentliche Funktionen doch deren Einsatz ist nur als privates Implementierungsdetail vorgesehen. Es darf also keine Apps geben die, explizit COM nutzen, aber die Implementierung von Unterkomponenten dürfen sehr wohl auf diese Features frei zugreifen.

Fazit

Es ist schon witzig. Ich kritisiere gerne Linux und den POSIX Standard, weil viel zu oft nicht auf Threads eingegangen wird und Funktionen in globale Variablen schreiben, die multithreading effektiv verhindern.

Doch Windows als Multithreading-Umgebung hat es wieder durch Einschränkungen und spezielle Initialisierungen extrem erschwert, dass man flüssig mit Threads arbeiten kann.

Und somit leiden viele moderne C++ Technologien wie std::async darunter, dass man sie keinesfalls mit bestimmten Systemroutinen kreuzen darf.

So gesehen bin ich mit beiden Ansätzen in Windows und Linux unglücklich. Ich verstehe zwar, dass es immer “historische” Gründe für den Istzustand gibt, dennoch ärgert es mich, dass ich seit über 13 Jahren in jedem neuen Projekt gezwungen bin, Workarounds einzubauen, sobald man etwas vom Betriebssystem haben möchte.

Und noch jeder Versuch eine neue API auf den Markt zu bringen (von dotNET über RT bis hin zu den unzähligen inkompatiblen Linux-Bibliotheken), hat mir gezeigt, dass es eben immer noch etwas gibt, was durch die neue APIs nicht abgedeckt wird und letztlich kein Weg an den klassischen APIs vorbeiführt.

Aber keine Sorge, morgen wird sicher alles besser!