en / de
AI
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

C# Concurrency Teil 2: Die Gefahren von Multithreading

Letztes Mal haben wir den Unterschied angeschaut zwischen CPU-bound und IO-bound-Aufgaben um die richtigen Technologien wählen zu können.
Siehe C# Concurrency Teil 1: CPU-bound und IO-bound Tasks In diesem Teil sehen wir uns die Gefahren von Multithreading an. Pro Gefahr werden Lösungsvorschläge vorgestellt die in späteren Teilen der Blog-Serie ausführlicher erklärt werden.

Datakorruption

Problem

Wenn ein Thread Daten ändert während ein anderer Thread diese Daten zeitgleich liesst, dann kann es sein, dass der lesende Thread korrupte Daten erhält. Lese- und Schreibeinstruktionen in C# Quellcode werden letztendlich übersetzt in die Prozessor Assembly-Sprache. Der Compiler übersetzt eine C# Instruktion in mehrere Assembly Instruktionen. Die eine Instruktion in C# ist auf Prozessorebene nicht zwangsläufig Atomar und kann durch einen anderer Thread unterbrochen werden.

Lösung

Kritische Codeblöcke, die nicht unterbrochen werden dürfen, locken. Ein Lock sorgt dafür, dass die kritischen Sektion (gelockter Abschnitt) nur durch einen Thread gleichzeitig ausgeführt wird. Das Locken wird separat behandelt in dieser Serie.

Code Reentrancy

Problem

Durch die asynchrone Programmierung (z.B. mit async\await) ist ein alter Bekannter zurück: das mehrfach Ausführen der gleichen Methode auf dem gleichen Thread.

Weil während des Wartens in einer asynchronen Methode der Thread freigegeben wird (UI Thread), kann dieser die gleiche Methode wieder aufrufen.

In WinForms gab es das Problem durch Application.DoEvents(): ein Hack um das UI reaktiver zu machen. Mit async\await ist dieses Problem wieder da, nur sind sich viele dessen nicht bewusst.

Lösung 1

Das Steuerelement welche die Methode aufruft deaktivieren solange die Methode läuft.

Lösung 2

Die Methode mit einem Semaphore sperren. Das kann ein einfacher Boolean sein (z.B. methodInProgress) und braucht nicht Threadsafe zu sein.

Ein lock hat hier keine Wirkung, weil es der gleiche Thread ist welcher die Methode mehrfach aufruft.

Race condition

Problem

Wenn mehrere Threads mit den gleichen Daten arbeiten ist die Reihenfolge der Zugriffe der Threads nicht gegeben. So kann es sein, dass ein Wert durch den einen Thread schon gelöscht wurde bevor der andere Thread den Wert verarbeitet hat.

Lösung 1

Mit Threadsafe Collections arbeiten (z.B. Producer\Consumer Queue) um die Zugriffe gezielt zu serialisieren.

Lösung 2

Mit Thread Synchronisierungsmechanismen arbeiten (z.B. Barrier, CountDownEvent, Manual-\AutoResetEvent, SemaphoreSlim).

Cross thread violation

Problem

Es darf nur der Thread auf ein Steuerelement zugreifen welcher das Steuerelement auch kreiert hat (UI Thread). Wenn ein anderer Thread das Element versucht zu ändern gibt es eine Cross Thread Exception.

Lösung

Thread Remarshalling: den Aufruf umleiten auf den UI Thread. Die verschiedenen Technologien werden in diesem Blog besprochen.

Dead-Lock

Problem

Zwei Threads warten auf einander bis sie fertig sind oder einen Lock freigeben.

Lösung

Nie einen Event werfen oder eine Fremdmethode aufrufen innerhalb eines Lock und das Lock Objekt nie öffentlich zugängig machen.

Das blockierende Warten auf einen andereren Thread hat hohes Potential für einen Dead-Lock. Es gibt Situationen wo es nicht anders geht aber dann müssen gewisse Randbedingungen (z.B. ConfigureAwait(false)) eingehalten werden.

In diesem Blog wird das Dead-Lock Problem ausführlich analysiert und werden Best Practices vorgestellt die dafür sorgen, dass Threads einander so wenig wie möglich blockieren.

Threads die am Leben bleiben

Problem

Wenn Threads nicht sauber und kontrolliert heruntergefahren werden, kann es sein, dass diese am Leben bleiben.

Auch kann es vorkommen, dass ein Thread sich aufhängt. Der Thread kann dann nicht kontrolliert heruntergefahren werden (z.B. mit CancellationToken) weil er nicht mehr im Stande ist das Token zu überprüfen.

Diese Orphan-Threads beanspruchen unnötig Resourcen und können für Memory-Leaks sorgen. Diese Leichen sind mögliche Ursachen davor, dass ein Programm nicht heruntergefahren werden kann.

Lösungen

  • Threads mit dem vorgesehen Mechanismus (z.B. CancellationToken) herunterfahren und auch kontrollieren, ob der Thread tatsächlich gestoppt wurde.
  • Start-and-forget Threads vermeiden.
  • Threads als background Thread definieren: diese verhindern das Herunterfahren vom Programm nicht.
  • Ein nicht-Threadpool Thread kann mit Interrupt()\Abort() abgeschossen werden wenn er nicht mehr reagiert.

 

Verschluckte Exceptions

Problem

Bei TPL und async\await werden Exceptions im Task-Objekt gespeichert. Es ist aber dem übergeordneten Thread überlassen die Exceptions im Task-Objekt auszuwerten. Wird das vergessen, dann gehen diese Exceptions verloren.

Lösungen

  • Immer das Task Objekt eines fertigen Tasks auswerten.
  • async void nur für Eventhandlers anwenden.
  • Start-and-forget Threads vermeiden.
  • Exceptions schon bei der Quelle im Task selber abfangen und verarbeiten.
  • Einen TaskScheduler.UnobservedTaskException Hander installieren.

 

Exceptions beim Herunterfahren vom Programm

Problem

Wird ein Thread nicht richtig (oder zu spät) heruntergefahren, dann kann es sein, dass dieser noch unerwartet auf Ressourcen zugreift die durch den Haupt-Thread abgebaut sind. So kommt es zu Exceptions und unvorgesehenen Situationen wie ‚ab und zu‘ Hängern.

Lösungen

Siehe „Threads die am Leben bleiben“.

Overflow

Problem

Overflow kann auftreten, wenn der generierende Thread (der Producer-Thread) viel schneller Items generiert als der empfangende Thread (der Consumer-Thread) verarbeiten kann.

Lösungen

Thread-Safe Producer\Consumer Queue verwenden mit Throttle-Funktionalität.

Programmintransparenz

Problem

Threading ist schwierig(er) zu Durchschauen und zu Debuggen. Es kommt oft zu ‚ab und zu‘ Exceptions oder Hängern die nicht reproduzierbar sind und deren Ursachen nur mit Traces und viel Aufwand zu finden sind.

Lösung

Die erwähnte Gefahren von Multithreading machen bewusst, dass beim Einsatz von Multithreading und async/await Vorsicht geboten ist. Es sollte nur überlegt und in Einklang mit Patterns und best Practices angewendet werden.

Follow up

Nächstes Mal schauen wir uns die bewährte Thread Klasse an. Dieser Weg ist oft immer noch der einzige Weg um spezielle Probleme zu lösen, trotz Threadpool, TPL und async\await.

← Vorige Post
Nächster Post →
Kommentare

3 Antworten zu “C# Concurrency Teil 2: Die Gefahren von Multithreading”

  1. […] Teil 2 Die Gefahren von Multithreading wurde erklärt wie Datakorruption entsteht und dass man mit locking den Zugriff auf die Daten […]

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden

Copyright © 2025 Noser Engineering AG – Alle Rechte vorbehalten.

NACH OBEN
Privacy Policy Cookie Policy
Zur Webcast Übersicht