Einführung in Aufgabenwarteschlangen in C#
In der modernen Multithread-Programmierung ist die Aufgabenwarteschlange (task queue) ein fundamentales Muster zur Organisation asynchroner Datenverarbeitung. In C# (.NET) wird dieses Muster durch mehrere leistungsstarke Mechanismen implementiert, die es Entwicklern ermöglichen, Ausführungsthreads effizient zu verwalten und die Last zu verteilen.
Die Aufgabenwarteschlange löst das klassische Producer-Consumer-Problem (Erzeuger-Verbraucher), bei dem ein Teil des Codes Aufgaben erstellt und ein anderer sie verarbeitet. In diesem Artikel betrachten wir die wichtigsten Methoden zur Implementierung von Aufgabenwarteschlangen in C#: von einfachen Sammlungen bis hin zu modernen Kanälen (Channel).
Grundlegende Ansätze zur Implementierung von Aufgabenwarteschlangen
1. ConcurrentQueue — Threadsichere Warteschlange
Die einfachste und grundlegendste Implementierung ist ConcurrentQueue<T> aus dem Namespace System.Collections.Concurrent. Sie bietet die threadsicheren Methoden Enqueue und TryDequeue.
using System;using System.Collections.Concurrent;using System.Threading.Tasks;
public class SimpleTaskQueue{ private readonly ConcurrentQueue<Action> _tasks = new(); private readonly Task _worker;
public SimpleTaskQueue() { _worker = Task.Run(ProcessTasks); }
public void Enqueue(Action task) { _tasks.Enqueue(task); }
private void ProcessTasks() { while (true) { if (_tasks.TryDequeue(out var task)) { task(); } } }}Vorteile:
- Einfachheit der Nutzung
- Geringer Speicherverbrauch
- Threadsicherheit ohne Sperren
Nachteile:
- Aktives Warten (busy waiting) in der Schleife
- Fehlende Signalisierung neuer Aufgaben
- Schwierigkeit beim Stoppen
2. BlockingCollection — Warteschlange mit Blockierung
BlockingCollection<T> löst das Problem des aktiven Wartens. Sie bietet die Methoden Add und Take, die den Thread blockieren, bis Daten verfügbar sind.
using System;using System.Collections.Concurrent;using System.Threading.Tasks;
public class BlockingTaskQueue : IDisposable{ private readonly BlockingCollection<Action> _tasks = new(new ConcurrentQueue<Action>()); private readonly Task[] _workers; private bool _disposed;
public BlockingTaskQueue(int workerCount = 4) { _workers = new Task[workerCount]; for (int i = 0; i < workerCount; i++) { _workers[i] = Task.Run(ProcessTasks); } }
public void Enqueue(Action task) { _tasks.Add(task); }
private void ProcessTasks() { foreach (var task in _tasks.GetConsumingEnumerable()) { if (_disposed) break; task(); } }
public void CompleteAdding() { _tasks.CompleteAdding(); }
public void Dispose() { _disposed = true; _tasks.CompleteAdding(); Task.WaitAll(_workers); _tasks.Dispose(); }}Wesentliche Merkmale:
- Automatische Blockierung des Threads bei fehlenden Aufgaben
- Unterstützung für Abbruch durch
CancellationToken - Möglichkeit zur Begrenzung der Warteschlangenkapazität
- Sicheres Beenden durch
CompleteAdding
Moderner Ansatz: Channel<T>
Seit .NET Core 3.0 und .NET 5 gibt es in C# System.Threading.Channels.Channel<T> — dies ist die leistungsfähigste und flexibelste Implementierung einer Aufgabenwarteschlange. Sie unterstützt asynchrone Operationen, Abbruch und Begrenzung des Durchsatzes