Lektion 3.11.2 hat gezeigt: Nur Parallelität macht Rechenarbeit schneller. Die Task Parallel Library (TPL) bringt dafür fertige Werkzeuge mit: Parallel.For und Parallel.ForEach für Schleifen, PLINQ für Abfragen. Sie verteilen die Arbeit auf die Kerne, sammeln Ausnahmen ein und lassen sich abbrechen. Was sie nicht tun: entscheiden, ob sich das lohnt. In dieser Lektion zählen Sie die Termine je Woche über drei Jahre auf vier Arten und messen. Eine Fassung ist zehnmal langsamer als die einfache Schleife, eine achtmal schneller, und PLINQ ist langsamer als gar keine Parallelität.

Parallel.For und Parallel.ForEach

Parallel.For(0, n, i => …) und Parallel.ForEach(items, item => …) sehen aus wie Schleifen, führen den Rumpf aber auf mehreren Threads des Pools gleichzeitig aus. Der aufrufende Thread arbeitet mit und kehrt erst zurück, wenn alle Durchläufe fertig sind. Drei Dinge müssen Sie dabei wissen.

Die Reihenfolge ist offen. Durchlauf 7 kann vor Durchlauf 3 fertig sein. Der Rumpf darf deshalb nicht davon abhängen, was ein anderer Durchlauf schon getan hat.

Gemeinsamer Zustand ist das Problem. Schreiben mehrere Durchläufe in dieselbe Variable oder Sammlung, kommen sie sich in die Quere. Ein Dictionary verträgt das nicht; im besten Fall wirft es eine Ausnahme, im schlechtesten liefert es falsche Zahlen. Die beiden Auswege sind ein lock (Lektion 3.11.4), der die Durchläufe wieder hintereinander zwingt, oder Zustand je Aufgabe: Jede Aufgabe zählt für sich, und nur am Ende wird zusammengeführt. Parallel.ForEach hat dafür eine Überladung mit drei Teilen:

  • localInit legt den Zustand einer Aufgabe an, etwa ein eigenes Dictionary,
  • der Rumpf bekommt diesen Zustand und gibt ihn weiter,
  • localFinally führt ihn am Ende einer Aufgabe zusammen, einmal je Aufgabe statt einmal je Element.

Die Aufteilung entscheidet mit. Ist die Arbeit je Element klein, kostet das Verteilen mehr als das Rechnen. Partitioner.Create(0, n) zerlegt einen Bereich in Stücke, und jede Aufgabe bekommt ein ganzes Stück statt einzelner Elemente. Für große Mengen kleiner Arbeiten ist das fast immer der richtige Weg.