Paul Butcher: Seven Concurrency Models in Seven Weeks

Paul Butcher: Seven Concurrency Models in Seven Weeks

 

In dieser Folge von ClojureScript Podcast bin ich auf das Buch von Paul Butcher aufmerksam geworden. Mit dem Podcast zu starten, würde ich empfehlen. Darin erklärt Paul Butcher sehr gut den Unterschied zwischen "Concurrency" und "Parallelism", also "Gleichzeitigkeit" und "Parallelität".

In den Köpfen vieler Menschen seien das dieselben Dinge. Aber sie seien sehr unterschiedlich.

Paul Butcher sieht es so: "Parallelism" habe etwas mit der Domäne der Lösung zu tun. Es habe etwas damit zu tun, was man implementiere, um ein Problem zu lösen.

"Concurrency" habe mit der Problem-Domäne zu tun. Es sei das Problem, das man versuche zu lösen.

In seinem Buch beschreibt Paul Butcher sieben "Concurrency"-Modelle. Die Reihenfolge, in der er sie in seinem Buch platzierte, sei ein logischer Pfad durch die Lektionen in dem Buch. Die Lektionen eines jeden Kapitels seien auf den vorherigen Lektionen aufgebaut worden.

Paul Butcher denkt, ein guter Einstiegspunkt müsse das traditionelle "threads and locks"-Modell sein. Teilweise weil das das häufigste Modell sei, dem man begegne. Und Teilweise weil das das Model sei, auf dem die meisten anspruchsvollsten Systeme aufgebaut seien. Es sei eine gute Idee, die Basis zu verstehen, auf der man aufbaue.

Die einfachste Art und Weise sich "threads and locks" vorzustellen, sei, sich vorzustellen, was in der Hardware passiere, auf der man liefe. Da sei nur eine minimale Fassade drüber, damit man "threads and locks" in seiner Sprache benutzen könne.

AHA-Moment :-)
Ein Thread sei ein "Thread der Kontrolle". Es sei eine logische Sequenz von einer Operation, gefolgt von einer weiteren, gefolgt von einer weiteren ...
Der Thread könne so implementiert sein, dass er tatsächlich mehrere CPUs habe, jede CPU würde einen Thread ausführen. Oder es könne auch eine CPU sein, die zeitlich versetzt die Threads ausführe.

Aber aus der Sicht des Programmierers spiele das keine Rolle. Die zwei Dinge würden fast identisch aussehen.

Und "locks" sei der Mechanismus, durch den man einen Thread stoppe, den Ausführungen eines anderen Threads in die Quere zu kommen. Dies erreiche man durch gegenseitigen Ausschluss. Also, man habe einen Mechanismus, durch den man alle bis auf einen Thread vom Zugang zu einer Ressource im Speicher ausschließe, so dass der eine Thread weiß, was passiert und darauf vertrauen kann, dass es funktionieren wird.

Der Vorteil von der "threads and locks"-Methode sei, dass sie leicht zu verstehen sei. Die Implementierung in einer Sprache sei ziemlich dieselbe wie in einer anderen. Also, wenn man's in C verstanden habe, könne man dieses Wissen nehmen und zu Java oder Ruby oder welcher Sprache auch immer rüberbringen.

Und die "threads and locks"-Methode sei eine sehr dünne Schicht über dem, was der Computer tue. Man programmiere ziemlich nah am Metall und könne so ziemlich alles machen, was die Hardware theoretisch erlauben würde.

Der Nachteil von der "threads and locks"-Methode sei, dass es wirklich schwer sei, es akkurat und korrekt hinzukriegen.

Paul Butcher denkt, diese Tatsache sei es Wert, besonders hervorgehoben zu werden, weil manche meinten, sie wüssten, dass die "threads and locks"-Methode schwierig sei, ohne sich dessen Bewusst zu sein, wie schwierig "threads and locks" in Wirklichkeit sei.

Paul Butcher sagt, er habe viel Zeit damit verbracht, "threads and locks"-Code zu schreiben und denkt, dass es unmöglich sei, ein aus mehreren Threads bestehendes Programm zu schreiben mit der "threads and locks"-Methode und komplett zuversichtlich zu sein, dass das Programm akkurat sei.

Fast jeder "threads and locks"-Code werde Bugs haben. Der einzige "threads and locks"-Code, der vielleicht keine Bugs mehr habe, sei der Code, der wirklich sehr intensiv tief unten innerhalb der Grenzen des Betriebssystems ausgeführt werde. Weil der Code so intensiv ausgeführt und seit Jahren geschrieben und debugged werde, werde er wahrscheinlich ganz gut funktionieren.

Aber wenn man eine typische Anwendung programmiere und die "threads and locks"-Meghode verwende, denkt Pault Butcher, es sei garantiert, dass man am Ende mit Etwas dastehen werde, dass Bugs habe.

AHA-Moment :-)
Also, warum "threads and locks" Probleme habe, liefe im Wesentlichen auf eine Sache hinaus: gemeinsamer veränderlicher Zustand (shared mutable state).
Wann auch immer man eine Variable, etwas im Speicher, habe, das sowohl verändert werden kann, als auch zwischen zwei Threads geteilt wird, komme man in Schwierigkeiten!

Das impliziere, dass man zwei Wege habe, um dieses Problem anzugehen:

Ein Weg ist, den State (Zustand der Anwendung) nicht mehreren Threads gleichzeitig zugänglich zu machen, also nicht zu teilen. Wenn der State nur einem Thread zugänglich ist, hat man keine Probleme, weil es nur einen Thread gibt, der Zugang zu dem State hat, also kann ihm nichts in die Quere kommen.

Und das ist der Ansatz, den z.B. das "actor"-Modell verfolgt. Das "actor"-Modell vermeidet es, den State zu teilen und vermeidet so die Probleme, die ein mehreren Threads zugänglicher State verursacht.

Funktionale Programmierung löst das Problem nicht, indem sie das Teilen des State  vermeidet, sondern indem sie den veränderbaren State (mutable state) vermeidet. Wenn man die Situation hat, dass sobald man etwas in den Speicher geschrieben hat, es sich nie wieder verändern wird - und das garantiert eine pure funktionale Sprache - hat man keine Probleme, weil, obwohl der State mehreren Threads zugänglich ist, es für einen Thread nicht möglich ist, die Daten zu modifizieren, die von einem anderen Thread kreiert wurden, weil kein Thread (kein Teil des Programms) jemals Daten modifizieren kann. Darum geht's bei Funktionaler Programmierung: das Vermeiden von Mutationen (avoiding mutation), also das Vermeiden vom Verändern von Etwas, das in den Speicher geschrieben wurde.

 

... Fortsetzung folgt.

Paul Butcher geht im Verlauf des Buches auf folgende Modelle ein (zu jedem Modell gibt es Code-Beispiele in einer für dieses Modell am besten geeigneten Sprache):

  • Threads and Locks (Java)
  • Functional Programming (Clojure)
  • The Clojure Way - Separating Identity from State (Clojure)
  • Actors (Elixir)
  • Communicating Sequential Processes (Golang, Clojurescript)
  • Data Parallelism (OpenCL)
  • The Lambda Architecture

the glider - universal hacker symbol