.NET Parallelität: Das richtige Werkzeug wählen
Wann dieses Skill verwenden
Verwenden Sie dieses Skill, wenn:
- Sie entscheiden müssen, wie Sie nebenläufige Operationen in .NET handhaben
- Sie abwägen, ob Sie async/await, Channels, Akka.NET oder andere Abstraktionen einsetzen
- Sie versucht sind, Locks, Semaphore oder andere Synchronisationsprimitive zu verwenden
- Sie Datenströme mit Backpressure, Batching oder Debouncing verarbeiten müssen
- Sie Zustände über mehrere nebenläufige Entitäten hinweg verwalten
Referenzdateien
- advanced-concurrency.md: Akka.NET Streams, Reactive Extensions, Akka.NET Actors (Entity-per-Actor, Zustandsautomaten, Cluster Sharding) und asynchrone lokale Funktionsmuster
Die Philosophie
Fangen Sie einfach an und eskalieren Sie nur bei Bedarf.
Die meisten Parallelitätsprobleme können mit async/await gelöst werden. Greifen Sie nur dann zu ausgefeilteren Werkzeugen, wenn Sie einen spezifischen Bedarf haben, den async/await nicht sauber adressieren kann.
Vermeiden Sie nach Möglichkeit gemeinsamen veränderlichen Zustand. Der beste Weg, mit Parallelität umzugehen, ist, sie wegzudesignen. Unveränderliche Daten, Nachrichtenaustausch und isolierter Zustand (wie bei Aktoren) eliminieren ganze Kategorien von Fehlern.
Locks sollten die Ausnahme sein, nicht die Regel. Wenn Sie gemeinsamen veränderlichen Zustand nicht vermeiden können:
- Erste Wahl: Umgestalten, um ihn zu vermeiden (Unveränderlichkeit, Nachrichtenaustausch, Aktor-Isolation)
- Zweite Wahl:
System.Collections.Concurrentverwenden (ConcurrentDictionary usw.) - Dritte Wahl:
Channel<T>verwenden, um den Zugriff durch Nachrichtenaustausch zu serialisieren - Letzter Ausweg:
lockfür einfache, kurzlebige kritische Abschnitte verwenden
Entscheidungsbaum
Was versuchen Sie zu tun?
│
├─► Auf I/O warten (HTTP, Datenbank, Datei)?
│ └─► async/await verwenden
│
├─► Eine Sammlung parallel verarbeiten (CPU-gebunden)?
│ └─► Parallel.ForEachAsync verwenden
│
├─► Producer/Consumer-Muster (Arbeitswarteschlange)?
│ └─► System.Threading.Channels verwenden
│
├─► UI-Ereignisbehandlung (Debounce, Throttle, Kombinieren)?
│ └─► Reactive Extensions (Rx) verwenden
│
├─► Serverseitige Stream-Verarbeitung (Backpressure, Batching)?
│ └─► Akka.NET Streams verwenden
│
├─► Zustandsautomaten mit komplexen Übergängen?
│ └─► Akka.NET Actors (Become-Muster) verwenden
│
├─► Zustand für viele unabhängige Entitäten verwalten?
│ └─► Akka.NET Actors (Entity-per-Actor) verwenden
│
├─► Mehrere asynchrone Operationen koordinieren?
│ └─► Task.WhenAll / Task.WhenAny verwenden
│
└─► Nichts davon passt?
└─► Fragen Sie sich: „Brauche ich wirklich gemeinsamen veränderlichen Zustand?“
├─► Ja → Umgestalten in Betracht ziehen, um ihn zu vermeiden
└─► Wirklich unvermeidbar → Channels oder Actors verwenden, um den Zugriff zu serialisieren
Stufe 1: async/await (Standardwahl)
Verwenden für: I/O-gebundene Operationen, nicht blockierende Wartevorgänge, die meisten alltäglichen Parallelitätsanforderungen.
// Einfaches asynchrones I/O
public async Task<Order> GetOrderAsync(string orderId, CancellationToken ct)
{
var order = await _database.GetAsync(orderId, ct);
var customer = await _customerService.GetAsync(order.CustomerId, ct);
return order with { Customer = customer };
}
// Parallele asynchrone Operationen (wenn unabhängig)
public async Task<Dashboard> LoadDashboardAsync(string userId, CancellationToken ct)
{
var ordersTask = _orderService.GetRecentOrdersAsync(userId, ct);
var notificationsTask = _notificationService.GetUnreadAsync(userId, ct);
var statsTask = _statsService.GetUserStatsAsync(userId, ct);
await Task.WhenAll(ordersTask, notificationsTask, statsTask);
return new Dashboard(
Orders: await ordersTask,
Notifications: await notificationsTask,
Stats: await statsTask);
}
Wichtige Prinzipien: Immer CancellationToken akzeptieren. ConfigureAwait(false) in Bibliothekscode verwenden. Nicht auf asynchronem Code blockieren.
Stufe 2: Parallel.ForEachAsync (CPU-gebundene Parallelität)
Verwenden für: Parallele Verarbeitung von Sammlungen, wenn die Arbeit CPU-gebunden ist oder Sie kontrollierte Parallelität benötigen.
public async Task ProcessOrdersAsync(
IEnumerable<Order> orders,
CancellationToken ct)
{
await Parallel.ForEachAsync(
orders,
new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount,
CancellationToken = ct
},
async (order, token) =>
{
await ProcessOrderAsync(order, token);
});
}
Wann NICHT verwenden: Reine I/O-Operationen, wenn die Reihenfolge wichtig ist, wenn Sie Backpressure benötigen.
Stufe 3: System.Threading.Channels (Producer/Consumer)
Verwenden für: Arbeitswarteschlangen, Producer/Consumer-Muster, Entkopplung von Produzenten und Konsumenten.
public class OrderProcessor
{
private readonly Channel<Order> _channel;
public OrderProcessor()
{
_channel = Channel.CreateBounded<Order>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait
});
}
// Producer
public async Task EnqueueOrderAsync(Order order, CancellationToken ct)
{
await _channel.Writer.WriteAsync(order, ct);
}
// Consumer (als Hintergrundaufgabe ausführen)
public async Task ProcessOrdersAsync(CancellationToken ct)
{
await foreach (var order in _channel.Reader.ReadAllAsync(ct))
{
await ProcessOrderAsync(order, ct);
}
}
public void Complete() => _channel.Writer.Complete();
}
Channels sind gut für: Entkopplung von Geschwindigkeit, Puffern mit Backpressure, Fan-Out an Arbeiter, Hintergrundwarteschlangen.
Channels sind NICHT gut für: Komplexe Stream-Operationen (Batching, Windowing), zustandsbehaftete Verarbeitung pro Entität, ausgefeilte Überwachung.
Stufe 4+: Akka.NET Streams, Reactive Extensions, Actors
Für fortgeschrittene Szenarien, die Stream-Verarbeitung, UI-Ereigniskomposition oder zustandsbehaftete Entitätenverwaltung erfordern, siehe advanced-concurrency.md.
Akka.NET Streams zeichnen sich durch serverseitiges Batching, Drosselung und Backpressure aus. Reactive Extensions sind ideal für die UI-Ereigniskomposition. Akka.NET Actors bewältigen Entity-per-Actor-Muster, Zustandsautomaten mit Become() und verteilte Systeme über Cluster Sharding.
Anti-Patterns: Was vermieden werden sollte
Locks für Geschäftslogik
// SCHLECHT: Locks zum Schützen von gemeinsamem Zustand verwenden
private readonly object _lock = new();
private Dictionary<string, Order> _orders = new();
public void UpdateOrder(string id, Action<Order> update)
{
lock (_lock) { if (_orders.TryGetValue(id, out var order)) update(order); }
}
// GUT: Einen Actor oder Channel verwenden, um den Zugriff zu serialisieren
Manuelles Thread-Management
// SCHLECHT: Threads manuell erstellen
var thread = new Thread(() => ProcessOrders());
thread.Start();
// GUT: Task.Run oder bessere Abstraktionen verwenden
_ = Task.Run(() => ProcessOrdersAsync(cancellationToken));
Blockieren in asynchronem Code
// SCHLECHT: Auf async blockieren - Deadlock-Risiko!
var result = GetDataAsync().Result;
// GUT: Async durchgängig
var result = await GetDataAsync();
Gemeinsamer veränderlicher Zustand ohne Schutz
// SCHLECHT: Mehrere Aufgaben mutieren gemeinsamen Zustand
var results = new List<Result>();
await Parallel.ForEachAsync(items, async (item, ct) =>
{
var result = await ProcessAsync(item, ct);
results.Add(result); // Wettlaufsituation!
});
// GUT: ConcurrentBag verwenden
var results = new ConcurrentBag<Result>();
Kurzreferenz: Welches Werkzeug wann?
| Bedarf | Werkzeug | Beispiel |
|---|---|---|
| Auf I/O warten | async/await | HTTP-Aufrufe, Datenbankabfragen |
| Parallele CPU-Arbeit | Parallel.ForEachAsync | Bildverarbeitung, Berechnungen |
| Arbeitswarteschlange | Channel<T> | Hintergrund-Job-Verarbeitung |
| UI-Ereignisse mit Debounce/Throttle | Reactive Extensions | Suchen-während-der-Eingabe, Auto-Speichern |
| Serverseitiges Batching/Drosselung | Akka.NET Streams | Ereignisaggregation, Ratenbegrenzung |
| Zustandsautomaten | Akka.NET Actors | Zahlungsabläufe, Auftragslebenszyklen |
| Entitätszustandsverwaltung | Akka.NET Actors | Auftragsverwaltung, Benutzersitzungen |
| Mehrere asynchrone Vorgänge auslösen | Task.WhenAll | Dashboard-Daten laden |
| Rennen mehrerer asynchroner Vorgänge | Task.WhenAny | Timeout mit Fallback |
| Periodische Arbeit | PeriodicTimer | Health Checks, Polling |
Der Eskalationspfad
async/await (hier beginnen)
│
├─► Parallelität nötig? → Parallel.ForEachAsync
│
├─► Producer/Consumer nötig? → Channel<T>
│
├─► UI-Ereigniskomposition nötig? → Reactive Extensions
│
├─► Serverseitige Stream-Verarbeitung nötig? → Akka.NET Streams
│
└─► Zustandsautomaten oder Entitätsverwaltung nötig? → Akka.NET Actors
Eskalieren Sie nur, wenn Sie einen konkreten Bedarf haben. Greifen Sie nicht „nur für den Fall“ zu Actors oder Streams.