Im Rahmen eines zweitägigen Workshops hat uns Luc Bläser, ehem. Informatik-Dozent an der OST, die wichtigsten Grundlagen rund um Parallel Programming and Concurrency mit .NET vermittelt. In folgendem Blogbeitrag haben wir unsere Learnings aus dem Workshop zusammengefasst. Der Umfang zeigt, wie komplex das Thema ist.

Inhalt

Einführung
     Level des Parallelismus
     Parallelismus vs. Concurrency
     Prozesse vs. Threads
     Prozess- und Thread-Scheduling
Thread Grundlagen
     Lifecycle
Critical Sections & Mutual Exclusion
Monitor
Beispiel: Bounded Buffer
Einige Synchronization Primitives
Concurrency Fehler
     Deadlock
     Livelock
     Starvation
     Alternativen zu Synchronisation
Thread-Pools and Task Parallel Library
     .NET Task Parallel Library
     Task-Parallelismus: Pitfalls
Data-Parallelismus mit TPL
     Data-Parallelismus
     Parallele Aggregation
     Parallel Partitioning
     Parallel LINQ (PLINQ)
Asynchrones Programmieren
     Async/Await
     Async Streams
     Async Dispose
     Asynchrones Programmieren: Pitfalls
Threading und GUI
     UI-Programmierung
.NET Memory Model
     Interlocked Klasse
     Optimierungen
Weitere Tools und Ressourcen

Einführung

Wieso benutzen wir Parallel Programming?

Vor allem zum ersten Punkt: Seit ca. 2003 gilt Moore's Law nicht mehr. Prozessoren werden nicht mehr immer schneller. Heutzutage bekommen wir eher mehr Prozessorkerne. Also brauchen wir mehr Threads, um dies auszunutzen.

Intel CPU Clock Rates
Level des Parallelismus

Hyper-Threading: Mehrere Registersets auf selbem CPU, der dann zwei Instruktionen gleichzeitig ausführen kann, wenn sich diese sich nicht gegenseitig in die Quere kommen. Wie zum Beispiel eine Ganzzahl- und eine Fliesskomma-Operation.

Multi-core: Mehrere Prozessorkerne auf demselben Chip, die sich dann je nach Architektur auch Cache teilen.

Multi-Prozessor: Mehrere Prozessorpackages in derselben Maschine. 

Computer-Netzwerk: Wenn mehrere Computer über ein Netzwerkprotokoll am selben Problem arbeiten, wie zum Beispiel bei Server-Farms.

Level des Parallelismus

Andere

Parallelismus vs. Concurrency

Parallelismus: Zerlegung eines Programms in Unterprogramme, die parallel auf mehreren Prozessoren ausgeführt werden können. => Schnellere Ausführung

Concurrency: Gleichzeitig oder verschachtelt ausgeführte Routinen, die auf gemeinsame Ressourcen zugreifen. => Effizientere Ressourcennutzung

Bei beiden haben wir typischerweise mehrere interagierende Prozesse oder Threads.


Prozesse vs. Threads

Prozess (heavy-weight)

Prozess (heavy-weight)

Thread (light-weight)

Thread (light-weight)

Prozess- und Thread-Scheduling

Kreierte Threads müssen auf einem Prozessorkern ausgeführt werden. Die Verantwortung zu verwalten, welcher Thread wann ausgeführt wird, fällt dem Thread-Scheduler zu. Dieser nimmt Threads, die in der Ready-Queue auf die Ausführung warten, weist sie einem Prozessor zu, reiht gerade ausführende oder wartende Threads wieder in diese Queue ein. So können auch auf Single-Core CPUs mehrere Threads anscheinend gleichzeitig ausgeführt werden (Prozessor Multiplexing). 

Prozess- und Thread-Scheduling

Ein Kontextswitch passiert, wenn ein Thread seine Ausführung unterbricht, dem Scheduler mitteilt, dass er jetzt gerade auch anderen Threads den Vortritt lassen möchte (Thread.Yield() in C#)  oder wenn der Scheduler nach einer ungewissen Zeit andere Threads "zum Zug kommen lässt".  Wenn der Scheduler so eingreift, nennt man das auch Preemption. 

Preemption


Thread Grundlagen

Jeder Prozess startet in einem Main Thread, der dann andere Threads erstellen kann, die unabhängig ausgeführt werden und selbst auch wieder neue Threads erstellen können. Auch ohne Threads bewusst zu nutzen, kann das Framework neue Threads erstellen. Zum Beispiel ist der .NET-Garbage-Collector ein eigener Thread.

Thread Grundlagen

Ein Thread wird instanziiert und mit der Start() Methode gestartet:

Threading Beispiel
var myThread = new Thread(() =>  {     for (int i = 0; i < 100; i++)  {         Console.WriteLine("MyThread step {0} ",i);     } }); myThread.Start(); // Andere arbeiten auf dem "Parent-Thread" myThread.Join(); // Wartet bis myThread beendet ist

Werden gerade mehrere Threads ausgeführt, kann man auf mit der Join() Methode warten bis ein bestimmter Thread fertig ist. So kann es zum Beispiel sein, das Thread 1 einen Thread 2 startet, der eine unabhängige Berechnung durchführt. Jedoch braucht Thread 1 das Resultat zu einem gewissen Zeitpunkt um fortzufahren. Mit der Join() Methode wird sichergestellt, das Thread 1 erst fortfährt, wenn Thread 2 das Resultat berechnet hat.

Thread 1 and 2


Ein Thread wird beendet wenn:

Nachdem ein Thread beendet wurde, kann er nicht mehr neu gestartet werden. Es muss stattdessen ein neuer Thread erstellt werden. 


Program Termination
Ein Programm terminiert sobald jeder Thread terminiert hat.

Ausnahme: wenn thread.IsBackground == true , dann wird nicht auf diesen gewartet (z.B. Garbage-Collector).


Passivierung
Als Programmierer können wir auch den Code durch Instruktionen verlangsamen.

Thread.Sleep(int milliSeconds) →  Suspendiert den Thread für die bestimmte Zeit.

Thread.Yield() →  Lässt andere Threads vor, ist aber meist unnötig, da aufgrund von Preemption auch so andere Threads zum Zug kommen.


Lifecycle

Lifecycle Thread

Critical Sections & Mutual Exclusion

Threads agieren mit demselben Heap, können also auf gemeinsame Objekte zugreifen. Dabei können sie sich aber auch gegenseitig in die Quere kommen. Wie in diesem Beispiel:

class BankAccount
{
 private int balance;
 
 public void Deposit(int amount)
 {
     // Begin Critical Section
 balance += amount;
 // End Critical Section
 }
}

Wenn nun zwei Threads die DepositMethode fast gleichzeitig aufgerufen werden, kann folgendes passieren:

DepositMethode


Am Ende ist der Kontostand bei 50, anstelle von 150.

Um dies zu vermeiden, gibt es in C# das lock Keyword. Dies synchronisiert die Zugriffe.

class BankAccount
{
 private readonly object sync = new();
 private int balance;


 public void Deposit(int amount)
 {
 lock (sync)
 {
 balance += amount;
 }
 }
}

Nun kann immer nur höchstens ein Thread innerhalb des lock Blocks sein. Somit kann das Data Race von oben nicht mehr auftreten.

Wir könnten auch this anstelle vom sync Objekt benutzen. Es wird aber laut Microsoft-Design-Konvention davon abgeraten. 


Monitor

Ein Monitor ist ein Objekt, dessen public Methoden alle synchronisiert sind. Dadurch kann sich jeweils nur ein Thread gleichzeitig im Monitor befinden. 

Für Monitors können wir auch komplexere Logik implementieren, bei der wir auf die Erfüllung von bestimmten Konditionen warten: Auch wenn wir eine Withdraw Methode zum BankAccount hinzufügen, muss erst genügend Geld auf einem Konto sein, bevor wir diese ausführen können. Für diesen Fall gibt es eine innere Warteschleife. Wenn eine solche Kondition noch nicht erfüllt ist, kann ein Thread mit Monitor.Wait(sync) das Lock freigeben und in die innere Warteschleife gehen. Dort wartet der Thread, bis ein anderer Thread Monitor.PulseAll(sync) oder Monitor.Pulse(sync) (nimmt nur einen Thread von der inneren Warteschleife) aufruft, und ihn wieder in die äussere Warteschleife einreiht. Monitor.Wait, Monitor.Pulse  und Monitor.PulseAll müssen alle zwingend innerhalb des lock Blocks aufgerufen werden.

class BankAccount
{
    private readonly object sync = new();
 private int balance;
 
 public void Withdraw(int amount)
 {
 lock (sync)
 {
 while(amount > balance)
 {
 Monitor.Wait(sync) // Wait inside Monitor
 }
 balance += amount; 
 }
 }
 
 public void Deposit(int amount)
 {
 lock (sync)
 {
 balance += amount;
 Monitor.PulseAll(sync) // Wake up all Waiters inside Monitor
 }
 }
}

Nach PulseAll müssen die wartenden Threads das Lock neu bekommen. In C#: Von der inneren Warteschleife gehen die Threads dann zuhinterst in die äussere Warteschleife (wahrscheinlich weil am einfachsten zu implementieren). Der Thread der "gepulsed" hat, behält aber das Lock und arbeitet weiter. Der nächste Thread kommt erst nach Exit des lock Blocks zum Zug.

Lock Blocks


Anstelle von lock(sync) kann auch Monitor.Enter und Monitor.Exit benutzt werden.

lock(sync)

Beispiel: Bounded Buffer

Hier gilt es zu beachten, dass die roten whiles nicht durch ifs ersetzt werden können, wie auch die Notwendigkeit PulseAll und nicht nur Pulse zu verwenden, zumindest wenn mehrere Consumers und Producers bestehen. Beide Male ist es so, dass wir nicht unterscheiden können, ob der Thread von der inneren Queue auch vom selben Typ ist. Wir wissen auch nicht, ob bereits andere Threads in der äusseren Queue warten, die die Konditionen wieder ändern. 


Einige Synchronization Primitives

Barrier: Wird benutzt wenn eine fixe Anzahl Threads sich einreihen muss, bis alle wieder freigegen werden. Nachdem die Barriere geöffnet wurde, schliesst sie sich sofort wieder selbst. Wie zum Beispiel bei einem Gokart-Rennen, wo alle wieder an den Start müssen, bis die nächste Runde gestartet wird.

Barrier

Count Down Events: Ungleich der Barriere, ist die Anzahl Threads nicht fix, aber CDE kann nicht wieder verwendet werden. Hier sind Signal und Wait separat. Also können Threads auch mehrere Signale senden. Es gibt auch AddCount(), aber das geht nur falls das CDE noch nicht offen ist. Ist wie ein Raketenstart: sobald released wird, können wir nicht mehr zurück und brauchen eine neue Rakete.

Semaphores: Eine Semaphore definiert eine bestimmte Anzahl Threads, die aktiv sein dürfen. Man könnte dies mit einem Parkplatz mit beschränkter Anzahl Plätzen beschreiben. Ist der Parkplatz voll, müssen weitere Autos (Threads) warten, bis ein Auto den Parkplatz verlässt und somit wieder einen Platz freigibt.

 Semaphores

In .NET sollte fast immer SemaphoreSlim, nicht Semaphore benutzt werden. Dieser ist innerhalb des Prozesses verwaltet und darum schneller als Semaphore, welches das OS benutzt. Semaphore kann nützlich sein, wenn mehrere Prozesse den gleichen Semaphore brauchen sollen (mit gleichen Namen).

Mutex: Ist ein Binärer Semaphore, der nur durch den Eigentümer wieder freigegeben werden kann. Dieser ist auch teuer wegen Kernelzugriff. 

Reader-Writer Lock: Enter/Exit Read/Write/UpgradableRead Lock

  Read

UpgradableRead

Write
Read
UpgradableRead
Write


Da C# keine const-Methoden hat, wird dies nicht oft verwendet.

ManualResetEvent: Manuell "Türchen" öffnen und schliessen. Selten gebraucht, da sehr manuell und auch nicht granular. Keine Garantien wie viele Threads durch gehen. Wie beim Sempaphore, gibt es auch hier ein ManualResetEventSlim, welches bevorzugt für Synchronisation innerhalb des selben Prozesses verwendet werden sollte.

AutoResetEvent: Manuell Türe für höchsten einen Waiter öffnen, aber es gibt keine Garantie, dass auch einer durchgeht.


Concurrency Fehler

Concurrency Fehler sind Halteproblem-schwer, also im generellen programmatisch nicht erkennbar und reproduzierbar (non-deterministic).  Es gibt aber statische Analysen zur Erkennung der meisten Fehler.

Race Condition: Bei einer Race Condition versuchen mehrere Threads dieselben Ressourcen zuzugreifen ohne ausreichend Synchronisation. Das führt zu möglichen fehlerhaften Verhalten oder Ergebnissen, meist aufgrund eines Data Race.

Data Race: Zwei gleichzeitige Zugriffe auf denselben Speicher. Dabei ist mindestens eine Write Operation vorhanden. 


Deadlock

Mehrere Threads locken sich gegenseitig, sodass keiner fortfahren kann.


Einfaches Deadlock Beispiel

Einfaches Deadlock Beispiel


Weiters Deadlock Beispiel

Weitere Deadlock Beispiel

Deadlock verhindern:


Livelock

Spezialfall des Deadlocks, aber CPU wird verbraucht (Spinlock).

Livelock


Starvation

Ein Thread bekommt keine Ressourcen. Nie hoffnungslos, kann aber möglicherweise für immer in innerer und äusserer Queue warten.


Alternativen zu Synchronisation

Immutability: Das Objekt hat ausschliesslich schreibgeschützte Felder.

Confinement: Das Objekt gehört höchstens einem Thread zur gleichen Zeit. Das innere Objekt wird in einem thread-sicheren äußeren Objekt verkapselt → keine Synchronisierung für innere Objekte erforderlich.

.NET-Datenstrukturen: Datenstrukturen in .Net sind grundsätzlich nicht thread-sicher. Es gibt aber Concurrent Collections: System.Collections.Concurrent.

Weitere Quellen von Concurrency: GC, Finalizers, Timers


Thread-Pools and Task Parallel Library

Task: Ein Objekt, dass potentiell parallel ausgeführten Code beschreibt. Tasks können parallel ausgeführt werden, müssen aber nicht.

Thread-Pool: Eine Queue für Tasks, die noch ausgeführt werden müssen. Ein Thread-Pool hat Limitierte Anzahl von Worker-Threads, die neue Tasks aus der Queue nehmen und komplett ausführen. Der Potentieller Parallelisierungsgrad wird durch Anzahl Worker-Threads bestimmt. Thread-Pools haben folgende Vorteile:


.NET Task Parallel Library

Effizienter Thread-Pool

Effizienter Thread-Pool

Task Start & Wait
Task task = Task.Run(() => { //code });
task.Wait(); // hier werden innere unhandled Exeptions geworfen

Task With Result
Task<int> task = Task.Run(() => {int total = //code; return total;});
int outerTotal = task.Result; // Blockiert bis ausgeführt

Run-to-Completion Principle und Thread-Injection
Task haben eine Limitation: Worker-Threads müssen einen Task bis zum Ende ausführen, ansonsten können sie keine anderen Tasks ausführen (Run to Completion Principle). Die Ausnahmen bilden hier geschachtelte Tasks und Fortsetzungen. Aus diesem Grund sollten Tasks nicht auf andere Tasks warten, ausser auf Untertasks und Fortsetzungen! Wenn dieses Prinzip verletzt wird, resultieren Performance Probleme oder Deadlocks (da limitierte Anzahl Threads). Wenn die Threads nicht weiter kommen (0.5s), fügt .NET weitere Threads dem Pool hinzu (Thread-Injection). Dies löst zwar Deadlocks, sollte aber nicht so verwendet werden (schlechtes Design).

Run-to-Completion Principle und Thread-Injection


Nested Tasks
Tasks können ineinander verschachtelt werden:

Task.Run(() => {
  var left = Task.Run(() => //left );
  var right = Task.Run(() => //right);
  int result = left.Result + right.Result;
});

Task Continuation
Tasks können auch verkettet werden → Tasks definieren nachfolgende Tasks, die nach ihrer Erledigung ausgeführt werden:

Task<User[]> usersTask = server.GetUsersAsync();
usersTask.ContinueWith(t => Display(t.Result));

Task Continuation
Multi-Continuation
Mit Task.WhenAll() kann mit der Ausführung des Programms gewartet werden bis gewisse Tasks abschlossen sind.

Task.WhenAll(task1, task2).ContinueWith(continuation)

Multi-Continuation
Mit Task.WhenAny() wird nur auf den ersten Task gewartet.

Task.WhenAny(task1, task2).ContinueWith(continuation)

Task.WhenAny(task1, task2).ContinueWith(continuation)
Exceptions
Calls zu .Wait() oder .Result werfen AggregateExceptions, wenn innerhalb des Tasks ein geworfene Exception nicht gefangen wurde. Man kann die Exceptiones auch etwas schöner haben mit task.GetAwaiter().GetResult();, dann bekommt man sofort die innere Exception und der Stack ist auch ansehbarer.

Wenn niemand .Wait() oder .Resultaufruft, "verpuffen" diese Exceptions (Fire-and-Forget). Der GC fängt die Exception dann stillschweigend als “unobserved Exception” auf. Es gibt einen Eventhandler: TaskScheduler.UnobseredTaskException wird erst nach Garbage-Collection und Finalisierung ausgeführt.


Cooperative Task Cancellation
Um einen Task zu canceln muss dem Task ein Cancellation-Token übergeben werden.

Cooperative Task Cancellation


Work-Stealing Thread-Pool
Jeder Thread im Pool hat auch nochmals eigene Queue um globale Queue zu entlasten. Wenn ein Thread im Pool keine Arbeit mehr hat und auch auf der globalen Task-Queue keine Tasks sind, kann ein Thread auch Tasks von anderen Threads nehmen.

Work-Stealing Thread-Pool


TPL Tuning Heuristics
Die TPL misst den Throughput. Darauffolgend werden dynamisch mehr oder weniger Threads für den Thread-Pool erstellt. Es wird empfohlen Tasks möglichst kurz zu halten oder falls nötig, Tasks als long-running zu bezeichnen (dann wird ein eigener Thread erstellt).

Task.Factory.StartNew(action, TaskCreationOptions.LongRunning);

Task-Parallelismus: Pitfalls

Data-Parallelismus mit TPL

Data-Parallelismus

Unabhängige Statements →Parallel.Invoke()

Parallel.Invoke(
    () => MergeSort(start, middle),
    () => MergeSort(middle, end)
);

Data-Parallelismus


Unabhängige LoopsParallel.For() und Parallel.ForEach()

Parallel.For(0, array.Length, 
    i => DoComputation(array[i])
);
Parallel.Foreach(list,
    file => Convert(file)
);

Data-Parallelismus

Stop Parallel Loops
Das Ergebnis wurde gefunden und keine weiteren Tasks müssen prozessiert werden.

Stop Parallel Loops


Break Parallel Loops
Überspringe alle weiteren Iterationsschritte und führe alle Tasks der vorangegangenen Iterationsschritte Schritte aus.

Break Parallel Loops


Parallele Aggregation

Wenn die Loops aggregiert werden, muss man wieder sicher stellen, dass keine Race Conditions entstehen.

Falsche Parallele Aggregation:

Falsche Parallele Aggregation


Korrekte Parallele Aggregation:

Korrekte Parallele Aggregation


Für eine möglichst effiziente Aggregation, kann jeder Thread auch sein eigenes Subtotal behalten und erst ganz am Ende synchronisiert zum Gesamttotal hinzufügen.

Threads


Parallel Partitioning

Da es nicht effizient wäre, aus jedem Loop einen Task zu generieren, gruppiert die TPL die Loops automatisch in Tasks auf Grundlage der momentanen Systemauslastung.

Parallel Partitioning


Ein Partitioner kann auch manuell gesetzt werden mit Partitioner.Create().

Partitioner


Parallel LINQ (PLINQ)

Sehr Deskriptiv und funktioniert sehr gut bei bereits bestehendem LINQ code.

.AsParallel() → Ausführung in zufälliger Reihenfolge

.AsOrdered() → Auf diese Weise kann die Eingabereihenfolge beibehalten werden. Statement folgt auf .AsParallel().AsParallel() .AsOrdered()


PLINQ: Resultate Pipen

PLINQ: Resultate Pipen

Oder mit den LINQ Extension-Methoden

var query  = bookCollection.AsParallel().Where(book => book.Title.Contains("Concurrency"));


Asynchrones Programmieren

Synchrone Calls: Normale Methoden, welche blockieren bis sie fertig ausgeführt sind. Nachdem ein Thread in die Methode gegangen ist, muss diese komplett durchgeführt werden. Dies ist ineffizient für zum Beispiel IO Operationen oder bei Netzwerkaufrufen. 

Asynchrone Calls: Lange Calls werden in separate Threads ausgelagert mit dem Ziel, dass der Caller-Thread nicht blockiert bleibt.

var task = Task.Run(
    // LongOperation
);


// perform other work
int result = task.Result;

Asynchrone Calls

Async/Await

Keyword async für Methoden →  Der Caller muss nicht für die gesamte Dauer der asynchronen Methode blockiert sein.

Keyword await für Tasks → Warte asynchron auf den Return des TPL-Tasks und nimm, falls zutreffend, das .Result()des Tasks. 

Async/Await


Returntypen

ref und out Parameter sind nicht möglich bei async Methoden.


Regeln

Grund: spezielles, verstecktes Ausführungsmodell für async/await


Async/await Ausführungsmodell
Bis zum ersten await (dass noch nicht bereit ist) wird die Methode synchron ausgeführt. Der Rest der Methode wird dann asynchron ausgeführt. Im Hintergrund zerlegt der Compiler die Methode in Sektionen, die ähnlich wie task.ContinueWith(); funktionieren. Die Sektionen nach dem await werden vom Thread-Pool aus ausgeführt.

Async/await Ausführungsmodell

Caller ohne Synchronisations-Kontext


Async Streams

IAsyncEnumberable ermöglicht asynchrone Iteration (jedes Item kann “awaited” werden).

Async Streams


Async Dispose

Pendant zum synchronen Disposable interface. Gibt einen ValueTask zurück (z.B. für database.CloseAsync();

Async Dispose


Asynchrones Programmieren: Pitfalls


Threading und GUI

Nur ein dedizierter UI-Thread darf auf die GUI Komponente zugreifen. (Grund: keine Synchronisation nötig → responsiver und einfacher)

Threading und GUI

UI-Programmierung

Mit async/await und Synchronisations-Kontext können wir die ganze Logik in einer Methode beschreiben und müssen den Code nicht fragmentieren wie in der klassischen UI-Programmierung vor C#5.0.

UI-Programmierung

Caller mit Synchronisations-Kontext


Klassischer Fehler: Nie Task.Result() oder Task.Wait() in UI-Thread benutzen → Deadlock


UI-Timers
Es gibt UI-Timers (System.Windows.Threading.DispatchTimer / System.Windows.Forms.Timer) für Handlers die GUI-Elemente beeinflussen. Diese geben die Delegates in die UI-Thread Event Queue, anstatt sie selbst auszuführen. 


.NET Memory Model

Interlocked Klasse

Das Lesen oder Schreiben von 1, 2 oder 4-byte Variablen ist atomar (kann in einem Schritt ausgeführt werden). Die meisten Instruktionen in C# sind aber nicht atomar und daher potentiell nicht thread-sicher. Für gewisse nicht-atomare Instruktionen gibt es in der Interlocked Klasse ein atomares Gegenstück. Beispielsweise:


Optimierungen

Instruktionen können neu angeordnet werden (vom Compiler, vom Runtime-System (JIT Compiler) oder vom CPU). Um die Integrität der Programmsemantik aufrechtzuerhalten und die erwarteten Ergebnisse zu gewährleisten, bietet C# für Concurrency das Konzept der "as if serial"-Semantik. Das bedeutet, dass Anweisungen zwar zu Optimierungszwecken umgeordnet werden können, das Endergebnis jedoch so aussehen sollte, als ob die Anweisungen in einer seriellen, einfädigen Weise ausgeführt würden. Mit anderen Worten, die seriellen Effekte jedes isolierten Threads sollten beibehalten werden, und das Gesamtverhalten sollte mit einer hypothetischen sequentiellen Ausführung übereinstimmen. 

Die Neuanordnung von Instruktionen kann auch eingeschränkt werden.

Neuanordnung verhindern mit Full-Fence
Alle lock acquire and releases (Monitor, Semaphore, etc.)

Neuanordnung verhindern mit Full-Fence


Neuanordnung verhindern mit Partial-Fence
Wenn ein Feld als volatil deklariert wurde, wird sichergestellt, dass Lese- und Schreibzugriff auf das Feld immer in der Reihenfolge erfolgen, wie sie im Code angegeben sind.

Neuanordnung verhindern mit Partial-Fence


Spinlock
Ein Synchronisationsprinzip, dass es Threads ermöglicht den Zugriff auf eine gemeinsame Ressource, durch Spinning (Busy-Waiting), zu synchronisieren. Dabei wird CPU-Zeit verbrennt wenn auf das lock gewartet wird. In C# existieren die SpinLock und die SpinWait Klasse. Die Verwendung von Thread.Yield() ist aber in der Praxis in ausgelasteten Loops meist schneller.


Weitere Tools und Ressourcen

Parallel Checker
Luc Bläser, der Referent,  hat ein VS-plugin geschrieben, welches viele Fälle erkennt und die Benutzer warnt. Das Tool kann auf Parallel Checker , auf Nuget oder in den VS-Extensions gefunden werden.


Parallel Helper
Concurrency bug pattern Erkennung, ergänzend zu Parallel Checker. Das Tool kann auf Parallel Helper, auf Nuget oder in den VS-Extensions gefunden werden.


Antipatterns
10 Antipattern: Parallel Code Smells