Task Parallel Library Ralf Abramowitsch Vector Informatik GmbH abramowitsch@lehre.dhbw-stuttgart.de
Moore s Gesetz: Alle 2 Jahre verdoppelt sich die Leistung von CPUs Quelle: http://de.wikipedia.org/wiki/mooresches_gesetz
Herb Sutter s The Free Lunch Is Over http://www.gotw.ca/pu blications/concurrency -ddj.htm
Programmierung für Multi-Core CPU s Multi-Processing Multi-Threading Programme müssen für Multi-Core angepasst werden
Modell über die Beschleunigung von Programmen durch parallele Ausführung Geschwindigkeitszuwachs durch den sequentiellen Anteil des Problems beschränkt, da sich dessen Ausführungszeit durch Parallelisierung nicht verringern lässt
Erklärung Speedup: S Sequentieller Anteil: (1-P) Paralleler Anteil: (P / N) Aufwand für Synchronisierung: o(n) Konsequenz P muss groß genug sein, damit die Aufwände für Parallelisierung nicht deren Nutzen auffressen
Tasks nutzen den.net Thread Pool Minimaler Aufwand zum Verwenden von Threads Load Balancing Rückgabe von Threads an Thread Pool Thread Pool Threads limitiert ThreadPool.GetMaxThreads Maximale Anzahl Threads in Pool ThreadPool.GetAvailableThreads Noch verfügbare Threads im Pool
Pro Prozess ein Thread Pool Neue Anforderungen über Warteschlange: QueueUserWorkItem Muss eine Anforderung länger als eine halbe Sekunde warten, wird neuer Pool-Thread erzeugt Maximal jedoch GetMaxThreads Wird Pool-Thread 30 Sekunden nicht benötigt, wird Thread aus dem Pool entfernt
Thread Pools in.net Klassenbibliothek: Asynchrone Aufrufe Socket-Verbindungen I/O-Completion Ports Timer
Einschränkungen Thread-Priorität kann nicht geändert werden Aufgaben dürfen nicht lange laufen bzw. andere Threads blockieren Thread Pool Threads können nicht identifiziert werden (z.b. Join)
Es gibt zwei Möglichkeiten: System.Linq.ParallelEnumerable-Klasse AsParallel-Erweiterungsmethode Ziel: Parallele Datenabfragen mit LINQ
string[] cities = { "Stuttgart", "Ludwigsburg", "Berlin", "Köln", "Hamburg", "München", "Leipzig", "Frankfurt" }; var q = from x in cities.asparallel() where x.length < 7 orderby x select x;
System.Threading.Tasks.Parallel Aufteilung von Schleifen in Threads aus dem Thread Pool Voraussetzung Schleifendurchläufe sind unabhängig voneinander Problem z.b.: a[i] = a[i 1] * 2
Thread-Synchronisierung an Schleifenende Thread-Local Variablen Die Laufvariable darf nicht als lokale Variable deklariert werden
Parallelisierung von foreach Reihenfolge nicht deterministisch string[] cities = { "Stuttgart", "Ludwigsburg", "Berlin", "Köln", "Hamburg", "München", "Leipzig", "Frankfurt" }; Parallel.ForEach(cities, item => { Console.WriteLine("Die Stadt {0} hat {1} Zeichen.", item, item.length); });
Codebeispiel: Parallelisieren einer Schleife Codebeispiel: Performance-Messung Codebeispiel: Parallelisierung einer unausgewogenen Schleife
Unausgewogene Schleifen for(int i = 0; i < 1000; i++) { if(i < 500) { DoLongCalculation(); } else { DoShortCalculation(); } }
Schleifen-Partitionierung kleine Arbeitseinheiten Sobald ein Thread mit einer Arbeitseinheit fertig ist, bekommt er die nächste, bis die Arbeit komplett erledigt ist
Codebeispiel: Partitioner
Achtung bei schreibendem Zugriff auf Klassenvariablen oder statischen Variablen Keine Fehlermeldungen (vom Compiler, Runtime) Ergebnisse sind falsch ( Data Race ) REGEL: Es darf nur ein Thread gleichzeitig schreibend zugreifen Ggf. synchronisieren, d.h., schreibende Zugriffe werden nacheinander abgewickelt Achtung: Performance!
Schleifendurchläufe sind nicht unabhängig voneinander D.h., in Schleifendurchlauf #i wird auf Daten zugegriffen, die im Schleifendurchlauf #j berechnet werden, wobei gilt: i!= j Z.B.: a[i] = a[i-1] + 1 Abhilfe: Den Algorithmus ändern Werte, die in der Schleife benötigt werden, möglichst aus dem Index berechnen
Zusammenfassen von Ergebnissen Locking double sum = 0.0; for(int i = 0; i < 1000; i++) { double y = Math.Sqrt(i); lock() { sum += y; } }
Zwischenwerte werden über den sog. ThreadLocalState weitergegeben Ablauf: Vor der Schleifenabarbeitung: LocalState initialisieren Schleifenteil abarbeiten: Im Thread rechnen, LocalState benutzen Nach der Abarbeitung der Teilschleife: Aggregation (Zusammenfassung) der Ergebnisse (Achtung: Locking!!!)
Das Ziel ist, möglichst WENIG Locking-Operationen durchzuführen Möglichkeit #1: In jedem Schleifendurchlauf beim Zugriff (Addition) auf dsum findet ein Lock statt Möglichkeit #2: Zwischensumme einer Arbeitseinheit ermitteln, Zwischensumme mit Locking zu dsum addieren
int sum = 0; // Parallel.For(from, to, init, body, finally); Parallel.For(0, 1000, () => 0, (i, pls, tls) => { return tls += i; }, (partsum) => { Interlocked.Add(ref sum, partsum); });
Codebeispiel: Aggregation
Wenn mehrere Threads für die Schleife parallel laufen, dann müssen alle Threads beendet werden Klasse ParallelLoopResult benutzen Berechnung beendet? Klasse ParallelLoopState enthält Break- Methode und eine Stop-Methode Alle Threads werden beendet
Codebeispiel: Schleife abbrechen
Tipps & Tricks Parallel.For und Parallel.ForEach: (Anzahl der Durchläufe * Dauer) muss groß genug sein Nicht vergessen: Amdahl s Gesetz! Möglichst wenig Synchronisierung verwenden
Die Anzahl der zu verwendenden Threads wird mit der ParallelOptions Klasse definiert MaxDegreeOfParallelism = -1 Alle Kerne und Prozessoren werden ausgenutzt In bestimmten Fällen kann es auch sinnvoll sein, die Anzahl der benutzten Threads auf > Kerne * Prozessoren zu setzen
Codebeispiel: Festlegen der Thread auf 2
Unabhängiger Code kann parallel ausgeführt werden Häufigstes Problem: Gleichzeitiger Zugriff auf die gleichen Daten (schreibend) Ohne Nachdenken geht da gar nichts! Wichtige Methode: Parallel.Invoke Auch hier: Unterschiedliche Schreibweisen möglich
Codebeispiel: Berechnung parallelisieren
Die Task-Klasse wird benötigt, um erweiterte Parallelisierungprobleme zu lösen Vergleichbar mit der ThreadPool-Klasse: ThreadPool.QueueUserWorkItem(delegate { }); ist ähnlich wie: Task.Factory.StartNew(delegate { });
Die TPL benutzt nun den.net ThreadPool als Standard-Scheduler Der ThreadPool hat mehrere Vorteile: Work-stealing queues werden intern in der TPL benutzt Hill-climbing-Methoden, wurden eingeführt, um die optimale Thread-Anzahl zu ermitteln Synchronisierungsmechanismen (wie SpinWait und SpinLock) werden intern verwendet
Wichtige Features: Synchronisierung von parallelen Ausführungseinheiten Task.Wait(), WaitAll(), WaitAny() Abbrechen von Ausführungseinheiten Cancelation Framework
Codebeispiel: Tasks
Übergabe von Parametern im Funktionsaufruf möglich Achtung: Seiteneffekte, wenn man es falsch macht Lösung 1: Übergabe der Parameter mit Parametern der Lambda-Funktion Lösung 2: Deklaration lokaler Variablen in der Schleife
Codebeispiel: Parameter und Tasks
Tasks können kontrolliert werden Created WaitingForActivation WaitingToRun RanToCompletion Canceled Faulted WaitingForChildrenToComplete
Tasks sind gut in sehr unausgewogenen Schleifen for(int i = 0; i < n; i++) { for(int j = 0; j < i; j++) { for(int k = 0; k < i; k++) { for(int l = 0; l < k; l++) { // Do Work } } } }
Lösung 1: Schleife (außen) mit Parallel.For parallelisieren Achtung: Die Schleifendurchläufe werden zum Schluss immer länger Der Scheduler kann die Teilschleifen nur schlecht aufteilen, so dass alle Prozessoren gleichmäßig ausgelastet sind Lösung: Schleifen rückwärts laufen lassen und Tasks benutzen (Task mit dem beiden inneren Schleifen)
Codebeispiel: Nested Loops Codebeispiel: Nested Loops Tasks
Die Klasse ermöglicht die asynchrone Berechnung von Daten Wenn später dann die berechneten Daten weiter benutzt werden sollen, wird geprüft, ob die Berechnung bereits abgeschlossen ist Sonst wird gewartet. = Synchronisierung Kann man Task<TResult>-Variablen in Anwendungen mit einer Benutzerschnittstelle benutzen?
Codebeispiel: Futures
Sequentielle Anwendung zu einer Zeit eine Exception Parallele Anwendung Mehrere Exceptions zu einem Zeitpunkt Reihenfolge nicht deterministisch
Alle aufgetretenen Exceptions werden in einem Objekt der Klasse System.Threading.AggregateException eingesammelt Wenn alle Threads angehalten sind, dann wird die AggregateException neu geworfen und kann bearbeitet werden Die einzelnen Exceptions können über das Property InnerExceptions abgefragt werden (eine Collection der aufgetretenen Exceptions)
Welche Klassen werfen AggregateException- Objekte? Die Parallel-Klasse Die Task-Klasse Die Task<TResult>-Klasse Parallel LINQ (Abfragen)
Codebeispiel: Concurrent Exceptions
Die Synchronisierung soll den Zugriff auf begrenzte Ressourcen des Rechners steuern, wenn mehrere Threads gleichzeitig darauf zugreifen wollen Variablen (RAM) Codeteile LPT-, COM- und USB-Schnittstellen
Eine Synchronisierung beinhaltet immer die Gefahr eines Deadlocks Thread A wartet auf die Resource Thread B hat die Resource in Benutzung Thread B wartet auf Thread A
Allgemeine Regeln: Nicht zu viel synchronisieren Langsam Nicht zu wenig synchronisieren Falsch Die richtige Sychronisierungsmethode wählen Keine eigenen Synchronisierungselemente programmieren Schwierig
Standard-Synchronisierungsmechanismen: Monitor, lock (C#) Interlocked-Klasse Mutex WaitHandle
Barrier CountDownEvent LazyInit<T> ManualResetEventSlim SemaphoreSlim SpinLock SpinWait WriteOnce<T> Collections (Stack, Queue, Dictionary)
Die goldene Regel gilt natürlich auch hier! WPF oder WinForms laufen im STA Sie dürfen nur aus dem Thread auf Controls zugreifen, im dem Sie die Controls erzeugt haben Ab Framework 2.0: Exception im Debug-Modus
WindowsForms: Benutzung von InvokeRequired zum Test, ob der richtige Thread Benutzung von BeginInvoke zum Umschalten in den richtigen Thread Windows Presentation Foundation: Benutzung von Dispatcher.VerifyAccess oder Dispatcher.CheckAccess zum Test, ob der richtige Thread Benutzung von Dispatcher.Invoke zum Umschalten in den richtigen Thread
Codebeispiel: TPL WinForms
Erweiterungen zusätzlich zu ContinueWith: ContinueWhenAll ContinueWhenAny Leistungsfähiges Konzept für die Fortführung der Arbeit, wenn bestimmte Teilaufgaben erledigt sind
Die alte Klasse TaskManager wurde ersetzt durch die TaskScheduler-Klasse Neue WorkStealing-Queues im ThreadPool TaskScheduler ist eine abstrakte Basisklasse Man kann davon ableiten und eigene TaskScheduler s programmieren Interessant für spezielle Szenarien Z.B.: spezielle Prioritätsverwaltung
Codebeispiel: Task Scheduler
Ein weiteres spezielles Szenario für die Benutzung eines TaskScheduler s: Zugriff auf das User Interface Die alte Regel gilt immer noch Ablauf: Asynchrone Berechnung im Task Fortführung mit ContinueWith (z.b. Ergebnisausgabe) aber im korrekten Thread-Kontext ( im UI-Thread) Früher: Benutzung von Invoke
Einfaches Abschießen (Kill) von Threads ist keine gute Lösung Offene Dateien??? Locks??? Schreibende Zugriffe??? Genutzte Resourcen??????
Das Beenden von Threads muss kooperativ erfolgen Der Thread kann nur an bestimmten Stellen beendet werden Der Thread kann selbst entscheiden, wann er beendet wird Abhängige Threads werden ebenfalls beendet Gutes Beispiel: BackgroundWorker-Control (ab.net Framework 2.0)
Anlegen eines CancellationTokenSource-Objektes Das CancellationToken-Objekt wird an alle Threads übergeben, die über das eine Source-Objekt beendet werden können In den Threads wird das Token-Objekt über das Property IsCancellationRequested abgefragt Alle Threads des Source-Objekts werden über die Cancel- Methode des Source-Objektes beendet
Ggf. wird dann aufgeräumt Nach dem Aufräumen wird aus dem Thread eine OperationCanceledException geworfen Diese kann im aufrufenden (Main-) Thread eingefangen und verarbeitet werden
Codebeispiel: Tasks abbrechen
ist in parallelen Programmen gar nicht so einfach Parallelprogrammierung hat meistens einen Grund: Performance-Steigerung Messgrößen sind also: Richtigkeit des Ergebnisses Ausführungszeit
Häufiges Problem: Die Anwendung skaliert nicht mehr mit Erhöhung der Prozessoranzahl 12 10 8 6 4 2 0 Zeit 1 2 3 4 5 6 7 8 9 10 11 12
Performance-Tests mit Visual Studio
Es gibt zwei neue Debugging-Fenster in Visual Studio 2010 Fenster: Parallel Tasks Fenster: Parallel Stacks Für native und für managed Code Die Fenster können geöffnet werden, wenn ein Breakpoint angelaufen wurde Debug > Windows > Parallel Tasks Debug > Windows > Parallel Stacks
Parallel Tasks-Fenster: Unterstützung des Task-basierten Programmierens Auflistung aller Tasks Waiting, Running, Scheduled, Ausgaben können konfiguriert und gruppiert werden
Parallel Stacks-Fenster: In modernen Anwendungen laufen oft mehrere Threads gleichzeitig Das Fenster zeigt die hierarchische Aufrufreihenfolge von Threads an siehe Call-Stack (ohne Threading-Info s) Ausgaben können in unterschiedlicher Weise dargestellt werden TopDown, BottomUp, Zooming, Panning,
Suche nach Deadlocks ist mit dem VS- Debugger möglich Benutzung des Parallel-Tasks-Fensters
Beispiel: Deadlock
OpenMP Für C, C++ MPI (Message Passing Interface) (C, C++, FORTRAN,.NET) Parallel Pattern Library (PPL) Eine Erweiterung für C++ in VS 2010
Cilk (Intel) C/C++ TBB (Intel Threading Building Blocks) C++ Parallelverarbeitung in der Grafikkarte CUDA (NVIDIA) OpenCL (KRONOS group) AMP (Microsoft)