Textversion des Berichts „Actors vs CSP vs Tasks ...“ mit C ++ CoreHard Autumn 2018

Anfang November war Minsk Gastgeber der nächsten C ++ - Konferenz C ++ CoreHard Herbst 2018-Konferenz. Sie lieferte einen Kapitänsbericht „Actors vs CSP vs Tasks ...“ , in dem dargelegt wurde, wie übergeordnete Anwendungen als „in C ++ aussehen können“. Bare Multithreading “, wettbewerbsfähige Programmiermodelle. Unter der geschnittenen Version dieses Berichts in einen Artikel umgewandelt. Gekämmt, stellenweise beschnitten, stellenweise ergänzt.

Ich möchte diese Gelegenheit nutzen, um der CoreHard- Community für die Organisation der nächsten großen Konferenz in Minsk und für die Gelegenheit zum Sprechen zu danken. Und auch für die zeitnahe Veröffentlichung von Videoberichten von Berichten auf YouTube .

Kommen wir also zum Hauptthema der Konversation. Welche Ansätze können wir verwenden, um die Multithread-Programmierung in C ++ zu vereinfachen, wie einige dieser Ansätze im Code aussehen, welche Funktionen bestimmten Ansätzen inhärent sind, was ihnen gemeinsam ist usw.

Hinweis: In der ursprünglichen Präsentation des Berichts wurden Fehler und Tippfehler gefunden. Daher werden im Artikel Folien aus der aktualisierten und bearbeiteten Version verwendet, die in Google Slides oder auf SlideShare zu finden sind .

Nacktes Multithreading ist böse!


Sie müssen mit der wiederholten Banalität beginnen, die jedoch immer noch relevant bleibt:
Multithread-C ++ - Programmierung über nackte Threads, Mutex und Bedingungsvariablen ist Schweiß , Schmerz und Blut .

Ein gutes Beispiel wurde kürzlich hier in diesem Artikel hier auf Habré beschrieben: " Architektur des Metaservers des mobilen Online-Shooters Tacticool ". Darin sprachen die Jungs darüber, wie sie es anscheinend geschafft haben, eine ganze Reihe von Rechen zu sammeln, die mit der Entwicklung von Multithread-Code in C und C ++ zusammenhängen. Es gab "Memory Passes" als Ergebnis von Rennen und geringe Leistung aufgrund erfolgloser Parallelisierung.

Infolgedessen endete alles ganz natürlich:
Nachdem wir einige Wochen damit verbracht hatten, die kritischsten Fehler zu finden und zu beheben, entschieden wir, dass es einfacher war, alles von Grund auf neu zu schreiben, als zu versuchen, alle Mängel der aktuellen Lösung zu beheben.

Die Benutzer haben C / C ++ gegessen, während sie an der ersten Version ihres Servers gearbeitet haben, und den Server in einer anderen Sprache neu geschrieben.

Eine hervorragende Demonstration, wie Entwickler in der realen Welt außerhalb unserer gemütlichen C ++ - Community die Verwendung von C ++ ablehnen, selbst wenn die Verwendung von C ++ noch angemessen und gerechtfertigt ist.

Aber warum?


Aber warum, wenn wiederholt gesagt wird, dass "nacktes Multithreading" in C ++ böse ist, verwenden die Leute es weiterhin mit Ausdauer, die einer besseren Anwendung würdig ist? Was ist schuld:

  • Unwissenheit?
  • Faulheit?
  • NIH-Syndrom?

Schließlich gibt es bei weitem keinen einzigen Ansatz, der durch die Zeit und viele Projekte getestet wurde. Insbesondere:

  • Schauspieler
  • Kommunikation sequentieller Prozesse (CSP)
  • Aufgaben (Async, Versprechen, Zukunft, ...)
  • Datenflüsse
  • reaktive Programmierung
  • ...

Es ist zu hoffen, dass der Hauptgrund immer noch Unwissenheit ist. Es ist unwahrscheinlich, dass dies an Universitäten gelehrt wird. Junge Berufstätige, die in den Beruf eintreten, nutzen das Wenige, das sie bereits kennen. Und wenn dann der Wissensspeicher nicht aufgefüllt wird, verwenden die Benutzer weiterhin nackte Threads, Mutexe und Bedingungsvariablen.

Heute werden wir über die ersten drei Ansätze aus dieser Liste sprechen. Und wir werden nicht abstrakt sprechen, sondern am Beispiel einer einfachen Aufgabe. Versuchen wir zu zeigen, wie der Code, der dieses Problem löst, mithilfe von Actor, CSP-Prozessen und -Kanälen sowie mithilfe von Task aussehen wird.

Herausforderung für Experimente


Es ist erforderlich, einen HTTP-Server zu implementieren, der:

  • akzeptiert die Anfrage (Bild-ID, Benutzer-ID);
  • gibt ein Bild mit "Wasserzeichen", das für diesen Benutzer einzigartig ist.

Beispielsweise kann ein solcher Server von einem kostenpflichtigen Dienst benötigt werden, der Inhalte per Abonnement verteilt. Wenn das Bild von diesem Dienst irgendwo auftaucht, können Sie anhand der Wasserzeichen erkennen, wer Sauerstoff blockieren muss.

Die Aufgabe ist abstrakt, sie wurde speziell für diesen Bericht unter dem Einfluss unseres Demo-Projekts Shrimp formuliert (wir haben bereits darüber gesprochen: Nr. 1 , Nr. 2 , Nr. 3 ).

Dies funktioniert auf unserem HTTP-Server wie folgt:

Nachdem wir eine Anfrage von einem Kunden erhalten haben, wenden wir uns an zwei externe Dienste:

  • Der erste gibt uns Benutzerinformationen zurück. Einschließlich von dort erhalten wir ein Bild mit "Wasserzeichen";
  • Die zweite gibt uns das Originalbild zurück

Beide Dienste arbeiten unabhängig voneinander und wir können gleichzeitig auf beide zugreifen.

Da die Verarbeitung von Anforderungen unabhängig voneinander erfolgen kann und sogar einige Aktionen bei der Verarbeitung einer einzelnen Anforderung parallel ausgeführt werden können, bietet sich die Nutzung der Wettbewerbsfähigkeit an. Am einfachsten fällt Ihnen ein, für jede eingehende Anfrage einen eigenen Thread zu erstellen:

Das One-Request = One-Workflow-Modell ist jedoch zu teuer und lässt sich nicht gut skalieren. Das brauchen wir nicht.

Selbst wenn wir uns der Anzahl der Workflows verschwenderisch nähern, brauchen wir immer noch eine kleine Anzahl davon:

Hier benötigen wir einen separaten Stream zum Empfangen eingehender HTTP-Anforderungen, einen separaten Stream für unsere eigenen ausgehenden HTTP-Anforderungen und einen separaten Stream zum Koordinieren der Verarbeitung empfangener HTTP-Anforderungen. Neben einem Pool von Workflows zum Ausführen von Vorgängen an Bildern (da die Manipulationen an Bildern gut parallel sind, wird durch die gleichzeitige Verarbeitung eines Bildes durch mehrere Streams die Verarbeitungszeit verkürzt).

Daher ist es unser Ziel, eine große Anzahl von gleichzeitig eingehenden Anforderungen auf einer kleinen Anzahl von Arbeitsthreads zu verarbeiten. Schauen wir uns an, wie wir dies durch verschiedene Ansätze erreichen.

Einige wichtige Haftungsausschlüsse


Bevor Sie mit der Hauptgeschichte fortfahren und Codebeispiele analysieren, müssen Sie einige Notizen machen.

Erstens sind alle folgenden Beispiele nicht an ein bestimmtes Framework oder eine bestimmte Bibliothek gebunden. Alle Übereinstimmungen in den Namen der API-Aufrufe sind zufällig und unbeabsichtigt.

Zweitens gibt es in den folgenden Beispielen keine Fehlerbehandlung. Dies geschieht bewusst, damit die Folien kompakt und sichtbar sind. Und damit das Material in die für den Bericht vorgesehene Zeit passt.

Drittens verwenden die Beispiele einen bestimmten Entitätsausführungskontext, der Informationen darüber enthält, was sonst noch im Programm vorhanden ist. Das Befüllen dieser Entität hängt vom Ansatz ab. Bei Akteuren enthält execute_context Links zu anderen Akteuren. Im Fall von CSP gibt es im Ausführungskontext CSP-Kanäle für die Kommunikation mit anderen CSP-Prozessen. Usw.

Ansatz 1: Schauspieler


Schauspieler Modell auf den Punkt gebracht


Bei Verwendung des Modells der Akteure wird die Lösung aus separaten Objekt-Akteuren aufgebaut, von denen jeder seinen eigenen privaten Zustand hat und auf den nur der Schauspieler selbst zugreifen kann.

Akteure interagieren über asynchrone Nachrichten miteinander. Jeder Akteur verfügt über ein eigenes Postfach (Nachrichtenwarteschlange), in dem an den Akteur gesendete Nachrichten gespeichert und von dort zur weiteren Verarbeitung abgerufen werden.

Schauspieler arbeiten nach sehr einfachen Prinzipien:

  • Ein Schauspieler ist eine Einheit mit Verhalten.
  • Akteure reagieren auf eingehende Nachrichten;
  • Nach Erhalt der Nachricht kann der Schauspieler:
    • Senden Sie eine (endgültige) Anzahl von Nachrichten an andere Akteure.
    • eine (endgültige) Anzahl neuer Akteure schaffen;
    • Definieren Sie ein neues Verhalten für die Verarbeitung nachfolgender Nachrichten.

Innerhalb einer Anwendung können Akteure auf verschiedene Arten implementiert werden:

  • Jeder Akteur kann als separater Betriebssystem-Stream dargestellt werden (dies geschieht beispielsweise in der C :: Just :: Thread Pro Actor Edition-Bibliothek).
  • Jeder Schauspieler kann als stapelbare Coroutine dargestellt werden.
  • Jeder Akteur kann als ein Objekt dargestellt werden, in dem jemand Rückrufmethoden aufruft.

In unserer Entscheidung werden wir Akteure in Form von Objekten mit Rückrufen verwenden und Coroutinen für den CSP-Ansatz belassen.

Entscheidungsschema basierend auf dem Modell der Akteure


Basierend auf den Akteuren sieht das allgemeine Schema zur Lösung unseres Problems folgendermaßen aus:

Wir werden Akteure haben, die am Anfang des HTTP-Servers erstellt werden und die ganze Zeit existieren, während der HTTP-Server arbeitet. Dies sind Akteure wie: HttpSrv, UserChecker, ImageDownloader, ImageMixer.

Nach Erhalt einer neuen eingehenden HTTP-Anforderung erstellen wir eine neue Instanz des RequestHandler-Akteurs, die nach einer Antwort auf die eingehende HTTP-Anforderung zerstört wird.

RequestHandler Actor Code


Die Implementierung des Request_Handler-Akteurs, der die Verarbeitung einer eingehenden HTTP-Anforderung koordiniert, kann folgendermaßen aussehen:
class request_handler final : public some_basic_type { const execution_context context_; const request request_; optional<user_info> user_info_; optional<image_loaded> image_; void on_start(); void on_user_info(user_info info); void on_image_loaded(image_loaded image); void on_mixed_image(mixed_image image); void send_mix_images_request(); ... //     . }; void request_handler::on_start() { send(context_.user_checker(), check_user{request_.user_id(), self()}); send(context_.image_downloader(), download_image{request_.image_id(), self()}); } void request_handler::on_user_info(user_info info) { user_info_ = std::move(info); if(image_) send_mix_images_request(); } void request_handler::on_image_loaded(image_loaded image) { image_ = std::move(image); if(user_info_) send_mix_images_request(); } void request_handler::send_mix_images_request() { send(context_.image_mixer(), mix_images{user_info->watermark_image(), *image_, self()}); } void request_handler::on_mixed_image(mixed_image image) { send(context_.http_srv(), reply{..., std::move(image), ...}); } 

Lassen Sie uns diesen Code analysieren.

Wir haben eine Klasse, in deren Attributen wir speichern oder speichern werden, was wir zur Verarbeitung der Anforderung benötigen. Auch in dieser Klasse gibt es eine Reihe von Rückrufen, die zu der einen oder anderen Zeit aufgerufen werden.

Wenn gerade ein Akteur erstellt wurde, wird zunächst der Rückruf on_start () aufgerufen. Darin senden wir zwei Nachrichten an andere Akteure. Erstens ist dies eine check_user-Nachricht, um die Client-ID zu überprüfen. Zweitens ist dies eine download_image-Nachricht zum Herunterladen des Originalbilds.

In jeder der gesendeten Nachrichten übergeben wir einen Link an uns selbst (ein Aufruf der self () -Methode gibt einen Link an den Akteur zurück, für den self () aufgerufen wurde). Dies ist notwendig, damit unser Schauspieler eine Nachricht als Antwort senden kann. Wenn wir beispielsweise in der check_user-Nachricht keinen Link zu unserem Akteur senden, weiß der UserChecker-Akteur nicht, an wen er die Benutzerinformationen senden soll.

Wenn eine user_info-Nachricht mit Benutzerinformationen als Antwort an uns gesendet wird, wird der Rückruf on_user_info () aufgerufen. Und wenn die image_loaded-Nachricht an uns gesendet wird, ruft unser Akteur den Rückruf on_image_loaded () auf. Und jetzt sehen wir in diesen beiden Rückrufen eine Funktion, die dem Modell der Akteure eigen ist: Wir wissen nicht genau, in welcher Reihenfolge wir Antwortnachrichten erhalten. Daher müssen wir unseren Code so schreiben, dass er nicht von der Reihenfolge abhängt, in der die Nachrichten ankommen. Daher speichern wir in jedem der Prozessoren zuerst die empfangenen Informationen im entsprechenden Attribut und prüfen dann, ob wir bereits alle benötigten Informationen gesammelt haben. Wenn ja, dann können wir weitermachen. Wenn nicht, werden wir weiter warten.

Aus diesem Grund haben wir ons in on_user_info () und on_image_loaded (), wenn send_mix_images_request () aufgerufen wird.

Grundsätzlich kann es in den Implementierungen des Modells der Akteure Mechanismen wie selektives Empfangen von Erlang oder Verstecken von Akka geben, über die Sie die Verarbeitungsreihenfolge eingehender Nachrichten manipulieren können. Wir werden heute jedoch nicht darüber sprechen, um nicht in den Dschungel der Details verschiedener Implementierungen des Modells einzutauchen Schauspieler.

Wenn also alle Informationen empfangen werden, die wir von UserChecker und ImageDownloader benötigen, wird die Methode send_mix_images_request () aufgerufen, bei der die Nachricht mix_images an den ImageMixer-Akteur gesendet wird. Der Rückruf on_mixed_image () wird aufgerufen, wenn wir eine Antwortnachricht mit dem resultierenden Bild erhalten. Hier senden wir dieses Bild an den HttpSrv-Akteur und warten, bis HttpSrv eine HTTP-Antwort bildet und den unnötigen RequestHandler zerstört (obwohl im Prinzip nichts den RequestHandler-Akteur daran hindert, sich im Rückruf on_mixed_image () selbst zu zerstören).

Das ist alles

Die Implementierung des RequestHandler-Akteurs erwies sich als recht umfangreich. Dies liegt jedoch daran, dass wir eine Klasse mit Attributen und Rückrufen beschreiben und dann auch Rückrufe implementieren mussten. Die Logik der Arbeit von RequestHandler ist jedoch sehr trivial, und es ist einfach, sie trotz der Menge an Code in der request_handler-Klasse zu verstehen.

Merkmale, die den Schauspielern eigen sind


Jetzt können wir ein paar Worte zu den Merkmalen des Modells der Schauspieler sagen.

Reaktoren


Akteure reagieren in der Regel nur auf eingehende Nachrichten. Es gibt Nachrichten - der Schauspieler verarbeitet sie. Keine Nachrichten - der Schauspieler tut nichts.

Dies gilt insbesondere für Implementierungen des Actors Model, bei denen Actors als Objekte mit Rückrufen dargestellt werden. Das Framework ruft den Rückruf des Akteurs ab. Wenn der Akteur die Kontrolle über den Rückruf nicht zurückgibt, kann das Framework keine anderen Akteure im selben Kontext bedienen.

Schauspieler sind überladen


Bei Schauspielern können wir Schauspieler und Produzenten sehr leicht dazu bringen, Nachrichten für Konsumenten-Schauspieler in einem viel schnelleren Tempo zu generieren, als Schauspieler-Konsumenten verarbeiten können.

Dies führt dazu, dass die Warteschlange eingehender Nachrichten für den Akteur-Verbraucher ständig wächst. Warteschlangenwachstum, d.h. Ein erhöhter Speicherverbrauch in der Anwendung verringert die Geschwindigkeit der Anwendung. Dies führt zu einem noch schnelleren Wachstum der Warteschlange, und infolgedessen kann sich die Anwendung verschlechtern und die Inoperabilität vollständig beeinträchtigen.

All dies ist eine direkte Folge der asynchronen Interaktion der Akteure. Weil der Sendevorgang im Allgemeinen nicht blockiert. Und es zu blockieren ist nicht einfach, weil Ein Schauspieler kann sich selbst schicken. Und wenn die Warteschlange für den Schauspieler voll ist, wird der Schauspieler beim Senden an sich selbst blockiert und dies beendet seine Arbeit.

Bei der Arbeit mit Schauspielern muss dem Problem der Überlastung ernsthafte Aufmerksamkeit gewidmet werden.

Viele Schauspieler sind nicht immer die Lösung.


Akteure sind in der Regel leichte Einheiten, und es besteht die Versuchung, sie in ihrer Anwendung in großer Anzahl zu erstellen. Sie können zehntausend Schauspieler und hunderttausend und eine Million erstellen. Und sogar hundert Millionen Schauspieler, wenn Eisen es Ihnen erlaubt.

Das Problem ist jedoch, dass das Verhalten einer sehr großen Anzahl von Akteuren schwer zu verfolgen ist. Das heißt, Möglicherweise haben Sie einige Schauspieler, die eindeutig richtig funktionieren. Einige Schauspieler, die entweder offensichtlich falsch oder gar nicht arbeiten, und Sie wissen es genau. Aber es kann eine große Anzahl von Schauspielern geben, über die Sie nichts wissen: Arbeiten sie überhaupt, funktionieren sie richtig oder falsch? Und das alles, denn wenn Sie hundert Millionen autonome Einheiten mit Ihrer eigenen Verhaltenslogik in Ihrem Programm haben, ist die Überwachung für alle sehr schwierig.

Daher kann sich herausstellen, dass wir beim Erstellen einer großen Anzahl von Akteuren in der Anwendung unser angewandtes Problem nicht lösen, sondern ein anderes Problem erhalten. Daher kann es für uns von Vorteil sein, einfache Akteure, die eine einzelne Aufgabe lösen, zugunsten komplexerer und schwererer Akteure, die mehrere Aufgaben ausführen, aufzugeben. Aber dann wird es weniger solche "schweren" Akteure in der Anwendung geben und es wird für uns einfacher sein, ihnen zu folgen.

Wo soll man suchen, was soll man mitnehmen?


Wenn jemand versuchen möchte, mit Schauspielern in C ++ zu arbeiten, macht es keinen Sinn, eigene Fahrräder zu bauen. Es gibt mehrere vorgefertigte Lösungen, insbesondere:


Diese drei Optionen sind lebendig, weiterentwickelnd, plattformübergreifend und dokumentiert. Sie können sie auch kostenlos testen. Weitere Optionen mit unterschiedlichem Grad an [nicht] Frische finden Sie in der Liste auf Wikipedia .

SObjectizer und CAF sind für die Verwendung in Aufgaben auf relativ hoher Ebene konzipiert, bei denen Ausnahmen und dynamischer Speicher angewendet werden können. Und das QP / C ++ - Framework kann für diejenigen von Interesse sein, die an der Embedded-Entwicklung beteiligt sind Unter dieser Nische ist er "eingesperrt".

Ansatz 2: CSP (Kommunikation sequentieller Prozesse)


CSP an den Fingern und ohne Matan


Das CSP-Modell ist dem Actors-Modell sehr ähnlich. Wir bauen unsere Lösung auch aus einer Reihe autonomer Entitäten auf, von denen jede ihren eigenen privaten Status hat und nur über asynchrone Nachrichten mit anderen Entitäten interagiert.

Nur diese Entitäten im CSP-Modell werden als "Prozesse" bezeichnet.

Prozesse in CSP sind leicht und ohne Parallelisierung ihrer Arbeit im Inneren. Wenn wir etwas parallelisieren müssen, starten wir einfach mehrere CSP-Prozesse, in denen es keine Parallelisierung mehr gibt.

CSP-Prozesse interagieren über asynchrone Nachrichten miteinander, aber Nachrichten werden nicht wie im Modell der Akteure an Postfächer gesendet, sondern an Kanäle. Kanäle können als Nachrichtenwarteschlangen betrachtet werden, die normalerweise eine feste Größe haben.

Im Gegensatz zum Schauspieler-Modell, bei dem für jeden Akteur automatisch ein Postfach erstellt wird, müssen Kanäle im CSP explizit erstellt werden. Und wenn wir die beiden Prozesse brauchen, um miteinander zu interagieren, müssen wir den Kanal selbst erstellen und dann dem ersten Prozess sagen "Sie werden hier schreiben", und der zweite Prozess sollte sagen: "Sie werden hier von hier aus lesen."

Gleichzeitig haben die Kanäle mindestens zwei Operationen, die explizit aufgerufen werden müssen. Die erste ist die Schreib- (Sende-) Operation, um eine Nachricht in den Kanal zu schreiben.

Zweitens ist es eine Lese- (Empfangs-) Operation, um eine Nachricht von einem Kanal zu lesen. Und die Notwendigkeit, Read / Receive explizit aufzurufen, unterscheidet CSP vom Actors Model, weil Im Fall von Akteuren kann die Lese- / Empfangsoperation im Allgemeinen vor dem Akteur verborgen sein. Das heißt, Das Actor Framework kann Nachrichten aus der Actor-Warteschlange abrufen und einen Handler (Callback) für die abgerufene Nachricht aufrufen.

Während der CSP-Prozess selbst den Zeitpunkt für den Lese- / Empfangsanruf auswählen muss, muss der CSP-Prozess bestimmen, welche Nachricht er empfangen hat, und die extrahierte Nachricht verarbeiten.

In unserer „großen“ Anwendung können CSP-Prozesse auf verschiedene Arten implementiert werden:

  • Der CSP-shny-Prozess kann als separates Thread-Betriebssystem implementiert werden. Es stellt sich als teure Lösung heraus, aber mit präventivem Multitasking;
  • Der CSP-Prozess kann durch Coroutine implementiert werden (stapelbare Coroutine, Faser, grüner Faden, ...). Es ist viel billiger, aber Multitasking ist nur kooperativ.

Ferner nehmen wir an, dass CSP-Prozesse in Form von stapelbaren Coroutinen dargestellt werden (obwohl der unten gezeigte Code möglicherweise in Betriebssystem-Threads implementiert ist).

CSP-basiertes Lösungsdiagramm


Das auf dem CSP-Modell basierende Lösungsschema ähnelt stark einem ähnlichen Schema für das Actors-Modell (und dies ist kein Zufall):

Es wird auch Entitäten geben, die am Start des HTTP-Servers beginnen und ständig funktionieren - dies sind die CSP-Prozesse HttpSrv, UserChecker, ImageDownloader und ImageMixer. Für jede neue eingehende Anforderung wird ein neuer RequestHandler-CSP-Prozess erstellt. Dieser Prozess sendet und empfängt dieselben Nachrichten wie bei Verwendung des Actors Model.

RequestHandler CSP-Prozesscode


Dies sieht möglicherweise aus wie der Code einer Funktion, die den CSP-schüchternen Prozess von RequestHandler implementiert:
 void request_handler(const execution_context ctx, const request req) { auto user_info_ch = make_chain<user_info>(); auto image_loaded_ch = make_chain<image_loaded>(); ctx.user_checker_ch().write(check_user{req.user_id(), user_info_ch}); ctx.image_downloader_ch().write(download_image{req.image_id(), image_loaded_ch}); auto user = user_info_ch.read(); auto original_image = image_loaded_ch.read(); auto image_mix_ch = make_chain<mixed_image>(); ctx.image_mixer_ch().write( mix_image{user.watermark_image(), std::move(original_image), image_mix_ch}); auto result_image = image_mix_ch.read(); ctx.http_srv_ch().write(reply{..., std::move(result_image), ...}); } 

Hier ist alles ziemlich trivial und wiederholt regelmäßig das gleiche Muster:

  • Zunächst erstellen wir einen Kanal zum Empfangen von Antwortnachrichten. Dies ist notwendig, weil Der CSP-Prozess verfügt nicht über ein eigenes Standardpostfach wie Akteure. Wenn der CSP-shny-Prozess etwas empfangen möchte, sollte dies durch die Erstellung des Kanals, in dem dieses "Etwas" geschrieben wird, verwirrt werden.
  • dann senden wir unsere Nachricht an den CSP-Master-Prozess. Und in dieser Nachricht geben wir den Kanal für die Antwortnachricht an;
  • Dann führen wir den Lesevorgang von dem Kanal aus, an den eine Antwortnachricht gesendet werden soll.

Dies wird am Beispiel der Kommunikation mit dem ImageSPixer CSP-Prozess sehr deutlich:
 auto image_mix_ch = make_chain<mixed_image>(); //  . ctx.image_mixer_ch().write( //  . mix_image{..., image_mix_ch}); //     . auto result_image = image_mix_ch.read(); //  . 

Aber separat lohnt es sich, sich auf dieses Fragment zu konzentrieren:
  auto user = user_info_ch.read(); auto original_image = image_loaded_ch.read(); 

Hier sehen wir einen weiteren gravierenden Unterschied zum Modell der Schauspieler. Im Fall von CSP können wir Antwortnachrichten in der für uns geeigneten Reihenfolge empfangen.

Möchten Sie zuerst auf user_info warten? Kein Problem, gehen Sie beim Lesen in den Ruhezustand, bis user_info angezeigt wird. Wenn image_loaded zu diesem Zeitpunkt bereits an uns gesendet wurde, wartet es einfach in seinem Kanal, bis wir es lesen.

Das ist in der Tat alles, was den oben gezeigten Code begleiten kann. CSP-basierter Code war kompakter als sein akteursbasiertes Gegenstück. Was seitdem nicht verwunderlich ist hier mussten wir keine separate Klasse mit Rückrufmethoden beschreiben. Und ein Teil des Zustands unseres CSP-schüchternen Prozesses RequestHandler ist implizit in Form der Argumente ctx und req vorhanden.

CSP-Funktionen


Reaktivität und Proaktivität von CSP-Prozessen


Im Gegensatz zu Akteuren können CSP-Prozesse reaktiv, proaktiv oder beides sein. Angenommen, der CSP-Prozess hat seine eingehenden Nachrichten überprüft und gegebenenfalls verarbeitet. Und als er sah, dass keine Nachrichten eingingen, verpflichtete er sich, die Matrizen zu multiplizieren.

Nach einiger Zeit war der CSP-Prozess der Matrix der Multiplikation überdrüssig und er suchte erneut nach eingehenden Nachrichten. Keine neuen? Okay, lass uns die Matrizen weiter multiplizieren.

Und diese Fähigkeit von CSP-Prozessen, auch ohne eingehende Nachrichten einige Arbeiten auszuführen, unterscheidet das CSP-Modell stark vom Actors-Modell.

Native Überlastschutzmechanismen


Da Kanäle in der Regel Warteschlangen von Nachrichten begrenzter Größe sind und der Versuch, eine Nachricht in einen gefüllten Kanal zu schreiben, den Absender stoppt, verfügen wir in CSP über einen integrierten Schutzmechanismus gegen Überlastung.

Wenn wir einen flinken Produzentenprozess und einen langsamen Konsumentenprozess haben, wird der Produzentenprozess den Kanal schnell füllen und für den nächsten Sendevorgang angehalten. Und der Producer-Prozess wird so lange schlafen, bis der Consumer-Prozess Speicherplatz im Kanal für neue Nachrichten freigibt. Sobald der Ort erscheint, wacht der Produzentenprozess auf und wirft neue Nachrichten in den Kanal.

Daher können wir uns bei der Verwendung von CSP weniger Gedanken über das Problem der Überlastung machen als im Fall des Modells der Akteure. Es stimmt, es gibt hier eine Falle, über die wir etwas später sprechen werden.

Wie werden CSP-Prozesse implementiert?


Wir müssen entscheiden, wie unsere CSP-Prozesse implementiert werden.

Dies kann so erfolgen, dass jeder CSP-shny-Prozess durch einen separaten Betriebssystem-Thread dargestellt wird. Es stellt sich als teure und nicht skalierbare Lösung heraus. Auf der anderen Seite erhalten wir präventives Multitasking: Wenn unser CSP-Prozess beginnt, Matrizen zu multiplizieren oder eine Art Blockierungsaufruf ausführt, wird das Betriebssystem ihn schließlich aus dem Rechenkern verdrängen und anderen CSP-Prozessen die Möglichkeit geben, zu arbeiten.

Es ist möglich, jeden CSP-Prozess durch eine Coroutine (stapelbare Coroutine) darzustellen. Dies ist eine viel billigere und skalierbare Lösung. Aber hier werden wir nur kooperatives Multitasking haben. Wenn der CSP-Prozess plötzlich die Matrixmultiplikation aufnimmt, wird der Arbeitsthread mit diesem CSP-Prozess und anderen damit verbundenen CSP-Prozessen blockiert.

Es kann einen anderen Trick geben. Angenommen, wir verwenden eine Bibliothek eines Drittanbieters, auf deren Innenseite wir keinen Einfluss haben. Und innerhalb der Bibliothek werden TLS-Variablen verwendet (d. H. Thread-lokaler Speicher). Wir rufen die Bibliotheksfunktion einmal auf und die Bibliothek legt den Wert einer TLS-Variablen fest. Dann "bewegt" sich unsere Coroutine zu einem anderen Arbeitsfaden, und das ist möglich, weil Im Prinzip können Coroutinen von einem Arbeitsthread zu einem anderen migrieren. Wir rufen die Bibliotheksfunktion wie folgt auf und die Bibliothek versucht, den Wert der TLS-Variablen zu lesen. Aber es kann schon eine andere Bedeutung geben! Und nach einem solchen Fehler zu suchen, wird sehr schwierig sein.

Daher müssen Sie die Wahl der Methode zur Implementierung von CSP-shnyh-Prozessen sorgfältig abwägen. Jede der Optionen hat ihre eigenen Stärken und Schwächen.

Viele Prozesse sind nicht immer die Lösung.


Wie bei den Akteuren ist die Fähigkeit, viele CSP-Prozesse in Ihrem Programm zu erstellen, nicht immer eine Lösung für ein angewandtes Problem, sondern schafft zusätzliche Probleme für sich.

Darüber hinaus ist eine schlechte Sichtbarkeit der Vorgänge im Programm nur ein Teil des Problems. Ich möchte mich auf eine weitere Falle konzentrieren.

Tatsache ist, dass Sie auf CSP-shnyh-Kanälen leicht ein Analogon von Deadlock erhalten können. Prozess A versucht, eine Nachricht auf den vollen Kanal C1 zu schreiben, und Prozess A wird angehalten. Von Kanal C1 sollte Prozess B gelesen werden, der versucht hat, auf Kanal C2 zu schreiben, der voll ist, und daher wurde Prozess B angehalten. Und von Kanal C2 sollte Prozess A gelesen werden. Das ist alles, wir haben einen Deadlock.

Wenn wir nur zwei CSP-Prozesse haben, können wir einen solchen Deadlock beim Debuggen oder sogar bei der Codeüberprüfung feststellen. Wenn wir jedoch Millionen von Prozessen im Programm haben, die aktiv miteinander kommunizieren, steigt die Wahrscheinlichkeit solcher Deadlocks erheblich.

Wo soll man suchen, was soll man mitnehmen?


Wenn jemand mit CSP in C ++ arbeiten möchte, ist die Auswahl hier leider nicht so groß wie für Schauspieler. Nun, oder ich weiß nicht, wo und wie ich suchen soll. In diesem Fall hoffe ich, dass die Kommentare andere Links teilen.

Wenn wir jedoch CSP verwenden möchten, müssen wir uns zunächst mit Boost.Fiber befassen . Es gibt Fasern (d. H. Coroutinen) und Kanäle und sogar Grundelemente auf niedriger Ebene wie Mutex, Bedingung_Variable, Barriere. All dies kann genommen und verwendet werden.

Wenn Sie mit CSP-Prozessen in Form von Threads zufrieden sind, können Sie sich SObjectizer ansehen . Es gibt auch Analoga von CSP-Kanälen und komplexe Multithread-Anwendungen auf SObjectizer können ohne Akteure geschrieben werden.

Schauspieler gegen CSP


Schauspieler und CSPs sind einander sehr ähnlich. Immer wieder stieß ich auf die Aussage, dass diese beiden Modelle einander gleichwertig sind. Das heißt, Was an Akteuren getan werden kann, kann in CSP-Prozessen fast 1: 1 wiederholt werden und umgekehrt. Sie sagen, dass es sogar mathematisch bewiesen ist. Aber hier verstehe ich nichts, also kann ich nichts sagen. Aber nach meinen eigenen Gedanken irgendwo auf der Ebene des alltäglichen gesunden Menschenverstandes sieht das alles ziemlich plausibel aus. In einigen Fällen können Akteure tatsächlich durch CSP-Prozesse und CSP-Prozesse durch Akteure ersetzt werden.

Es gibt jedoch verschiedene Unterschiede zwischen Akteuren und CSPs, anhand derer festgestellt werden kann, wo jedes dieser Modelle vorteilhaft oder nachteilig ist.

Kanäle gegen Mailbox


Ein Schauspieler hat einen einzigen „Kanal“ zum Empfangen eingehender Nachrichten - dies ist seine Mailbox, die automatisch für jeden Schauspieler erstellt wird. Und der Schauspieler ruft die Nachrichten von dort nacheinander ab, genau in der Reihenfolge, in der sich die Nachrichten in der Mailbox befanden.

Und das ist eine ziemlich ernste Frage. Angenommen, das Postfach des Schauspielers enthält drei Nachrichten: M1, M2 und M3. Der Schauspieler interessiert sich derzeit nur für M3.Aber bevor er zu M3 kommt, extrahiert der Schauspieler zuerst M1, dann M2. Und was wird er mit ihnen machen?

Auch im Rahmen dieses Gesprächs werden wir nicht auf die selektiven Empfangsmechanismen von Erlang und das Verstecken von Akka eingehen.

Während der CSP-shny-Prozess den Kanal auswählen kann, von dem er aktuell Nachrichten lesen möchte. Ein CSP-Prozess kann also drei Kanäle haben: C1, C2 und C3. Derzeit interessiert sich der CSP-Prozess nur für Nachrichten von C3. Diesen Kanal liest der Prozess. Und er wird zu den Inhalten der Kanäle C1 und C2 zurückkehren, wenn er daran interessiert ist.

Reaktivität und Proaktivität


Akteure sind in der Regel reaktiv und arbeiten nur, wenn sie eingehende Nachrichten haben.

Während CSP-Prozesse auch ohne eingehende Nachrichten einige Arbeit leisten können. In einigen Szenarien kann dieser Unterschied eine wichtige Rolle spielen.

Zustandsautomaten


Tatsächlich sind Akteure Finite-State-Maschinen (KA). Wenn es in Ihrem Fachgebiet viele Finite-State-Maschinen gibt und selbst wenn es sich um komplexe, hierarchische Finite-State-Maschinen handelt, können Sie diese daher viel einfacher auf der Grundlage des Akteurmodells implementieren, als indem Sie einem CSP-Prozess eine Raumfahrzeugimplementierung hinzufügen.

In C ++ gibt es noch keine native CSP-Unterstützung.


Die Erfahrung mit der Go-Sprache zeigt, wie einfach und bequem es ist, das CSP-Modell zu verwenden, wenn seine Unterstützung auf der Ebene einer Programmiersprache und ihrer Standardbibliothek implementiert wird.

In Go ist es einfach, "CSP-Prozesse" (auch bekannt als Goroutinen) zu erstellen. Es ist einfach, Kanäle zu erstellen und damit zu arbeiten. Es gibt eine integrierte Syntax für die gleichzeitige Arbeit mit mehreren Kanälen (Go-shny select, die nicht nur zum Lesen, sondern auch zum Schreiben funktioniert). Die Standardbibliothek kennt Goroutins und kann sie wechseln, wenn Goroutin einen blockierenden Aufruf von stdlib ausführt.

In C ++ gibt es bisher keine Unterstützung für stapelbare Coroutinen (auf Sprachebene). Daher kann die Arbeit mit CSP in C ++ stellenweise aussehen, wenn nicht sogar wie eine Krücke, dann ... Das erfordert sicherlich viel mehr Aufmerksamkeit für sich selbst als im Fall des gleichen Go.

Ansatz Nr. 3: Aufgaben (asynchron, Zukunft, wait_all, ...)


Über den aufgabenbasierten Ansatz in den häufigsten Worten


Die Bedeutung des aufgabenbasierten Ansatzes besteht darin, dass wir bei einer komplexen Operation diese Operation in separate Aufgabenschritte unterteilen, wobei jede Aufgabe (es handelt sich um eine Aufgabe) eine einzelne Unteroperation ausführt.

Wir starten diese Aufgaben mit der speziellen Operation async. Die asynchrone Operation gibt ein zukünftiges Objekt zurück, in das nach Abschluss der Aufgabe der von der Aufgabe zurückgegebene Wert platziert wird.

Nachdem wir N Aufgaben gestartet und N Objekte-Zukunft erhalten haben, müssen wir das alles irgendwie in einer Kette zusammenfügen. Es scheint, dass nach Abschluss der Aufgaben Nr. 1 und Nr. 2 die von ihnen zurückgegebenen Werte in Aufgabe Nr. 3 fallen sollten. Und wenn Aufgabe Nr. 3 abgeschlossen ist, sollte der zurückgegebene Wert an die Aufgaben Nr. 4, Nr. 5 und Nr. 6 übertragen werden. Usw. usw.

Für eine solche "Krawatte" werden spezielle Mittel verwendet. Wie zum Beispiel die .then () -Methode eines zukünftigen Objekts sowie die Funktionen wait_all (), wait_any ().

Eine solche Erklärung „an den Fingern“ ist möglicherweise nicht sehr klar. Fahren wir also mit dem Code fort. Vielleicht wird in einem Gespräch über einen bestimmten Code die Situation klarer (aber keine Tatsache).

Request_handler-Code für den aufgabenbasierten Ansatz


Der Code zum Verarbeiten einer eingehenden HTTP-Anforderung basierend auf Aufgaben kann folgendermaßen aussehen:
 void handle_request(const execution_context & ctx, request req) { auto user_info_ft = async(ctx.http_client_ctx(), [req] { return retrieve_user_info(req.user_id()); }); auto original_image_ft = async(ctx.http_client_ctx(), [req] { return download_image(req.image_id()); }); when_all(user_info_ft, original_image_ft).then( [&ctx, req](tuple<future<user_info>, future<image_loaded>> data) { async(ctx.image_mixer_ctx(), [&ctx, req, d=std::move(data)] { return mix_image(get<0>(d).get().watermark_image(), get<1>(d).get()); }) .then([req](future<mixed_image> mixed) { async(ctx.http_srv_ctx(), [req, im=std::move(mixed)] { make_reply(...); }); }); }); } 

Versuchen wir herauszufinden, was hier passiert.

Zunächst erstellen wir eine Aufgabe, die im Kontext unseres eigenen HTTP-Clients gestartet werden soll und Informationen über den Benutzer anfordert. Das zurückgegebene zukünftige Objekt wird in der Variablen user_info_ft gespeichert.

Als nächstes erstellen wir eine ähnliche Aufgabe, die auch im Kontext unseres eigenen HTTP-Clients ausgeführt werden sollte und die das Original-Image lädt. Das zurückgegebene zukünftige Objekt wird in der Variablen original_image_ft gespeichert.

Als nächstes müssen wir warten, bis die ersten beiden Aufgaben abgeschlossen sind. Was wir direkt aufschreiben: when_all (user_info_ft, original_image_ft). Wenn beide zukünftigen Objekte ihre Werte erhalten, führen wir eine weitere Aufgabe aus. Diese Aufgabe nimmt die Bitmap mit dem Wasserzeichen und dem Originalbild und führt eine weitere Aufgabe im Kontext von ImageMixer aus. Diese Aufgabe mischt Bilder und wenn sie abgeschlossen ist, wird eine andere Aufgabe im HTTP-Serverkontext gestartet, die eine HTTP-Antwort generiert.

Vielleicht ist eine solche Erklärung dessen, was im Code passiert, nicht viel geklärt. Lassen Sie uns deshalb unsere Aufgaben nummerieren:

Und schauen wir uns die Abhängigkeiten zwischen ihnen an (aus denen die Reihenfolge der Aufgaben hervorgeht):

Und wenn wir dieses Bild jetzt auf unseren Quellcode legen, dann hoffe ich, dass es klarer wird:


Merkmale des aufgabenbasierten Ansatzes


Sichtbarkeit


Das erste Merkmal, das bereits offensichtlich sein sollte, ist die Sichtbarkeit des Codes in Task. Nicht alles ist gut mit ihr.

Hier können Sie so etwas wie die Rückrufhölle erwähnen. Die Programmierer von Node.j sind sehr vertraut damit. Aber auch C ++ - Spitznamen, die eng mit Task zusammenarbeiten, tauchen in diese Hölle des Rückrufs ein.

Fehlerbehandlung


Ein weiteres interessantes Feature ist die Fehlerbehandlung.

Einerseits kann es bei Verwendung von Async und Future mit der Übermittlung von Fehlerinformationen an die interessierte Partei noch einfacher sein als bei Akteuren oder CSP. Wenn im CSP-Prozess A eine Anforderung an Prozess B sendet und auf eine Antwortnachricht wartet, müssen wir entscheiden, wie der Fehler an Prozess A übermittelt werden soll, wenn B beim Ausführen der Anforderung auf einen Fehler stößt:

  • oder wir machen einen separaten Nachrichtentyp und einen Kanal für den Empfang;
  • oder wir geben das Ergebnis mit einer einzelnen Nachricht zurück, die für ein normales und fehlerhaftes Ergebnis std :: variante ist.

Und im Falle der Zukunft ist alles einfacher: Wir extrahieren aus der Zukunft entweder ein normales Ergebnis oder eine Ausnahme.

Andererseits können wir leicht auf eine Kaskade von Fehlern stoßen. Beispielsweise ist in Aufgabe Nr. 1 eine Ausnahme aufgetreten. Diese Ausnahme fiel in das zukünftige Objekt, das an Aufgabe Nr. 2 übergeben wurde. In Aufgabe Nr. 2 haben wir versucht, den Wert aus der Zukunft zu übernehmen, aber eine Ausnahme erhalten. Und höchstwahrscheinlich werden wir die gleiche Ausnahme rauswerfen. Dementsprechend wird es in die nächste Zukunft fallen, die zu Aufgabe Nr. 3 gehen wird. Es wird auch eine Ausnahme geben, die möglicherweise auch veröffentlicht wird. Usw.

Wenn unsere Ausnahmen protokolliert werden, sehen wir im Protokoll die wiederholte Wiederholung derselben Ausnahme, die von einer Aufgabe in der Kette zu einer anderen Aufgabe wechselt.

Aufgaben und Timer / Timeouts abbrechen


Ein weiteres sehr interessantes Merkmal der aufgabenbasierten Kampagne ist das Abbrechen von Aufgaben, wenn ein Fehler aufgetreten ist. Nehmen wir an, wir haben 150 Aufgaben erstellt, die ersten 10 erledigt und festgestellt, dass es keinen Sinn macht, die Arbeit fortzusetzen. Wie stornieren wir die verbleibenden 140? Dies ist eine sehr, sehr gute Frage :)

Eine andere ähnliche Frage ist, wie man Freunde mit Timern und Timeouts zu Aufgaben macht. Angenommen, wir greifen auf ein externes System zu und möchten die Wartezeit auf 50 Millisekunden beschränken. Wie können wir den Timer einstellen, wie wir auf den Ablauf des Timeouts reagieren, wie wir die Task-Kette unterbrechen, wenn das Timeout abgelaufen ist? Wieder ist Fragen einfacher als Antworten :)

Betrug


Nun, und um über die Funktionen des aufgabenbasierten Ansatzes zu sprechen. In dem gezeigten Beispiel wurde ein wenig betrogen:
  auto user_info_ft = async(ctx.http_client_ctx(), [req] { return retrieve_user_info(req.user_id()); }); auto original_image_ft = async(ctx.http_client_ctx(), [req] { return download_image(req.image_id()); }); 

Hier habe ich zwei Aufgaben an den Kontext unseres eigenen HTTP-Servers gesendet, von denen jeder eine Blockierungsoperation im Inneren ausführt. Um zwei Anforderungen an Dienste von Drittanbietern parallel verarbeiten zu können, mussten Sie hier Ihre eigenen Ketten asynchroner Aufgaben erstellen. Ich habe dies jedoch nicht getan, um die Lösung mehr oder weniger sichtbar zu machen und auf die Präsentationsfolie zu passen.

Schauspieler / CSP vs Aufgaben


Wir haben drei Ansätze untersucht und festgestellt, dass der aufgabenbasierte Ansatz keinem von ihnen ähnelt, wenn Akteure und CSP-Prozesse einander ähnlich sind. Und es könnte den Anschein haben, dass Actors / CSP im Gegensatz zu Task stehen sollten.

Aber ich persönlich mag eine andere Sichtweise.

Wenn wir über das Modell der Akteure und CSP sprechen, dann sprechen wir über die Zerlegung unserer Aufgabe. In unserer Aufgabe wählen wir separate unabhängige Entitäten aus und beschreiben die Schnittstellen dieser Entitäten: Welche Nachrichten senden sie, welche empfangen sie, über welche Kanäle gehen die Nachrichten.

Das heißt,In Zusammenarbeit mit Schauspielern und CSP sprechen wir über Schnittstellen.

Nehmen wir jedoch an, wir teilen die Aufgabe in separate Akteure und CSP-Prozesse auf. Wie genau machen sie ihren Job?

Wenn wir den aufgabenbasierten Ansatz aufgreifen, sprechen wir über die Implementierung. Informationen darüber, wie eine bestimmte Arbeit ausgeführt wird, welche Unteroperationen ausgeführt werden, in welcher Reihenfolge, wie diese Unteroperationen gemäß Daten verbunden sind usw.

Das heißt,Bei der Arbeit mit Task sprechen wir über die Implementierung.

Daher sind Schauspieler / CSP und Aufgaben nicht so sehr gegensätzlich, sondern ergänzen sich. Akteure / CSPs können verwendet werden, um Aufgaben zu zerlegen und Schnittstellen zwischen Komponenten zu definieren. Und Aufgaben können dann verwendet werden, um bestimmte Komponenten zu implementieren.

Wenn Sie beispielsweise Actor verwenden, haben wir eine Entität wie ImageMixer, die mit Bildern im Thread-Pool bearbeitet werden muss. Im Allgemeinen hindert uns nichts daran, den ImageMixer-Akteur für den aufgabenbasierten Ansatz zu verwenden.

Wo soll man suchen, was soll man mitnehmen?


Wenn Sie mit Aufgaben in C ++ arbeiten möchten, können Sie sich die Standardbibliothek des kommenden C ++ 20 ansehen. Sie haben der Zukunft bereits die Methode .then () sowie die freien Funktionen wait_all () und wait_any hinzugefügt. Siehe cppreference für Details .

Es ist auch schon weit von einer neuen async ++ Bibliothek entfernt . In dem gibt es im Prinzip alles, was Sie brauchen, nur ein bisschen mit einer anderen Sauce.

Und es gibt eine noch ältere Microsoft PPL- Bibliothek . Was auch alles gibt, was Sie brauchen, aber mit Ihrer eigenen Sauce.

Separate Ergänzung zur Intel TBB-Bibliothek. Es wurde in der Geschichte über den aufgabenbasierten Ansatz nicht erwähnt, da Taskgraphen von TBB meiner Meinung nach bereits ein Datenflussansatz sind. Und wenn dieser Bericht fortgesetzt wird, wird das Gespräch über Intel TBB sicherlich kommen, aber im Kontext der Geschichte über den Datenfluss.

Interessanter


Kürzlich gab es hier auf Habré einen Artikel von Anton Polukhin: "Wir bereiten uns auf C ++ 20 vor. Coroutines TS anhand eines realen Beispiels ."

Es geht darum, einen aufgabenbasierten Ansatz mit stapellosen Coroutinen aus C ++ 20 zu kombinieren. Und es stellte sich heraus, dass der Code auf der Grundlage der Lesbarkeit von Aufgaben der Lesbarkeit von Code in CSP-Prozessen nahe kam.

Wenn sich also jemand für den aufgabenbasierten Ansatz interessiert, ist es sinnvoll, diesen Artikel zu lesen.

Fazit


Nun, es ist Zeit, mit den Ergebnissen fortzufahren, da es nicht so viele davon gibt.

Die Hauptsache, die ich sagen möchte, ist, dass Sie in der modernen Welt möglicherweise nur dann Multithreading benötigen, wenn Sie ein Framework entwickeln oder eine bestimmte Aufgabe auf niedriger Ebene lösen.

Und wenn Sie Anwendungscode schreiben, benötigen Sie kaum nackte Threads, Synchronisationsprimitive auf niedriger Ebene oder eine Art sperrfreier Algorithmen sowie sperrfreie Container. Es gibt seit langem bewährte Ansätze, die sich bewährt haben:

  • Schauspieler
  • Kommunikation sequentieller Prozesse (CSP)
  • Aufgaben (Async, Versprechen, Zukunft, ...)
  • Datenflüsse
  • reaktive Programmierung
  • ...

Und vor allem gibt es in C ++ vorgefertigte Tools für sie. Sie müssen nichts radeln, Sie können es nehmen, versuchen und, wenn es Ihnen gefällt, in Betrieb nehmen.

So einfach: nehmen, versuchen und in Betrieb nehmen.

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


All Articles