Thread Kommunikation - Sauberes Design
-
Hallo Leute
Immer wenn ich Kommunikation zwischen Threads brauche, habe ich mir was zusammengehackt. Klappt auch soweit gut, nur möchte ich mal wissen wie das "richtig" geht.
Konkret nutze ich eine lock free queue. Jede Information an einen Thread wird ihm in Form von Messages geschickt. Der Sender-Thread stopft die Nachricht in die queue, der Empfänger holt sie ab... doch wie wird sie dekodiert?
Beispielcode Methode 1:
class SpecialMessage1; class SpecialMessage2; class Message { public: virtual SpecialMessage1 *SpecialMessage1Cast() { return 0; } virtual SpecialMessage2 *SpecialMessage2Cast() { return 0; } }; class SpecialMessage1 : public Message { public: SpecialMessage1 *SpecialMessage1Cast() { return this; } }; class SpecialMessage2 : public Message { public: SpecialMessage2 *SpecialMessage2Cast() { return this; } }; int main() { StartThread(); SendMessage(std::make_unique<SpecialMessage1>()); } void Thread1() { for (;;) { std::unique<Message> msg = WaitForMessage(); if (auto *ptr = msg->SpecialMessage1Cast()) { // do stuff 1 } else if (auto *ptr = msg->SpecialMessage2Cast()) { // do stuff 2 } } }
Vorteile:
- Logik des Threads ist gebündelt an einer Stelle und im Scope des jeweiligen Threads.Nachteile:
- Jede Nachricht braucht extra cast-Funktionen
- Hauptverarbeitung nutzt effektiv ein switch-case welches mit O(N) skaliertHinweis:
Die cast-Funktionen nutze ich um dynamic_cast zu umgehen. Nach einer persönlichen Horrorerfahrung mit dem Ding (Dateizugriff zur Laufzeit um Typen aus Debuginfos zu lesen) möchte ich diese Schnecke gerne vermeiden.Beispielcode Methode 2:
class SpecialMessage1; class SpecialMessage2; class Message { public: virtual void Handle(); }; class SpecialMessage1 : public Message { public: void Handle() { // do stuff } }; class SpecialMessage2 : public Message { public: void Handle() { // do other stuff } }; int main() { StartThread(); SendMessage(std::make_unique<SpecialMessage1>()); } void Thread1() { for (;;) { std::unique<Message> msg = WaitForMessage(); msg->Handle(); } }
Vorteile:
- Dispatching in O(1)
- Übersichtlichkeit des ThreadsNachteile:
- Auslagerung der Logik aus dem Thread in die Messages
- Kein direkter Zugriff auf den Scope des ThreadsMomentan nutze ich Methode 1. Ich suche ein Design/Pattern welches die Logik nach Möglichkeit semantisch zum Thread bindet und in dessen Scope laufen kann (ohne Verrenkungen wie zig Parameter zu übergeben), allerdings übersichtlich und einfach zu erweitern ist. Jede Lösung ist akzeptabel, die beiden Codebeispiele sollen nur zeigen wie ich mir das ungefähr vorstelle.
-- IUnknown_struct
-
Die cast-Funktionen nutze ich um dynamic_cast zu umgehen. Nach einer persönlichen Horrorerfahrung mit dem Ding (Dateizugriff zur Laufzeit um Typen aus Debuginfos zu lesen)
Soso
-
Gefällt mir nicht. Warum kann der Thread überhaupt verschiedene Nachrichten verarbeiten?
Variante 1 gefällt überhaupt nicht. Ob das besser als dynamic_cast ist, sei mal dahingestellt, ab und zu macht man sowas tatsächlich (z.B. OpenSceneGraph), aber grundsätzlich ist das ziemlicher Blödsinn.
-
Also, Methode 2 ist was anderes als Methode 1. Eine Message schicke ich einem anderen Thread und der entscheidet dann, was er dann in Reaktion darauf tun will.
Bei Methode 2 entscheidet das aber der Sender - in diesem Fall würde man das dann eher "Task" nennen.
Die Methode zum Casten gefällt mir nicht. Message müsste schon alle abgeleiteten Klassen kennen. dynamic_cast ist zwar langsam, aber nicht so langsam, dass es in typischen Anwendungsfällen einen auch nur messbaren Unterschied machen würde.
-
Vielleicht sollte ich das Problem genauer beschreiben:
Ich habe mehrere Threads, z.b. einen für die GUI, einen der sich um Netzwerkzugriff kümmert und einer der für lang laufende Berechnungen gedacht ist.
Klickt der Nutzer jetzt auf einen Button bekommmt das der GUI Thread mit. Er schickt nun eine Message an den Netzwerkthread, z.b. MessageUpdateDatabase. Die Nachricht bekommt ein paar Parameter mit aber was der Netzwerkthread damit anfängt, ist nicht mehr im Scope der GUI. Deswegen muss die Unterscheidung der Nachrichten und Auswahl der Aktion im Netzwerkthread erfolgen.
Ist der Netzwerkthread fertig mit seinem laden geht eine Nachricht an den Berechnungsthread raus. Auch hier möchte ich die Threads bis auf das gemeinsame Message Interface voneinander trennen. Die Bearbeitung geht den Netzwerkthread also nichts mehr an.
Methode 2 hat den Nachteil dass die Logik was passieren muss in der Message steckt, und nicht im Thread der sie verarbeitet. Ich würde die Programmlogik gerne in den 3 Threads halten.
Welches Design würdet ihr stattdessen nehmen?
-
In Zeiten von C++11 werden keine selbst gebastelten Message-Klassen mehr zum nächsten Thread geschickt, sondern eine
std::function<void()>
. Um diese zu erzeugen eignen sich C++-Lambdas ganz hervorragend. Der lesende Thread führt dann einfach diese Funktionen aus.
Und wenn man es sich ganz einfach machen will und sicher sein möchte, dass das Queueing und das Lesen aus der Queue sicher und performant funktioniert, so nutze man gleich boost.asio.Anbei ein kleines Demo:
#include <boost/asio/io_service.hpp> #include <chrono> #include <iostream> #include <memory> #include <thread> int main() { using namespace std; { boost::asio::io_service io_service; auto worker = make_unique< boost::asio::io_service::work >( io_service ); thread thrd( [&io_service]() { io_service.run(); } ); io_service.post( []() { cout << "mache dies ... " << endl; } ); io_service.post( []() { cout << " ... oder das" << endl; for( auto i: { "1", "2", "3", "...", "viele"} ) { this_thread::sleep_for( chrono::milliseconds(200) ); cout << i << endl; } } ); // -- Beenden: this_thread::sleep_for( chrono::milliseconds(10) ); cout << "Programm beenden" << endl; worker.reset(); thrd.join(); } cout << "jetzt ist Schluss" << endl; return 0; }
Eine schöne Beschreibung dazu findet man bei gamedev.net.
Und für den Rückweg - sprich für das Resultat der Arbeit eines zweiten Threads - ist std::future angesagt.
Gruß
Werner
-
Die Idee mit std::function ist gut, das werde ich weiter verfolgen.
Das Problem ist weiterhin dass der Auszuführende Code nicht im Scope des Aufrufers liegt. Der Netzwerkcode bleibt im Netzwerkthread. Was natürlich gehen würde wäre das Aufrufen über eine Art RPC.
Der Aufrufende Thread würde eine std::function an den jeweiligen Thread senden. Wie wäre das?
std::future bringt hier nicht viel, da nicht auf jede "Nachricht" eine Antwort erfolgt