Aufgabenbasiertes asynchrones Muster (TAP) in .NET: Einführung und Übersicht

Das aufgabenbasierte asynchrone Muster ist in .NET das empfohlene Entwurfsmuster für neue Entwicklungen. Es basiert auf den Typen Task und Task<TResult> im System.Threading.Tasks-Namespace, mit denen asynchrone Vorgänge dargestellt werden.

Benennung, Parameter und Rückgabetypen

TAP verwendet eine einfache Methode, um die Initiierung und den Abschluss eines asynchronen Vorgangs darzustellen. Dies steht sowohl zum Muster des asynchronen Programmiermodells (Asynchronous Programming Model, APM oder IAsyncResult) als auch dem ereignisbasierten asynchronen Muster (Event-based Asynchronous Pattern, EAP) im Kontrast. APM erfordert die Methoden Begin und End. EAP erfordert eine Methode, die das Async-Suffix hat, und auch mindestens ein Ereignis, einen Ereignishandler-Delegattypen und aus EventArg abgeleitete Typen. Asynchrone Methoden im TAP umfassen das Suffix Async nach dem Vorgangsnamen für Methoden, die awaitable-Typen zurückgeben, z.B. Task, Task<TResult>, ValueTask und ValueTask<TResult>. Ein asynchroner Get-Vorgang, der einen Task<String> zurückgibt, kann z.B. mit GetAsync benannt werden. Wenn Sie eine TAP-Methode einer Klasse hinzufügen, die bereits eine EAP-Methode mit dem Async-Suffix enthält, verwenden Sie stattdessen das Suffix TaskAsync. Wenn beispielsweise die Klasse bereits über eine GetAsync-Methode verfügt, verwenden Sie den Namen GetTaskAsync. Wenn eine Methode einen asynchronen Vorgang startet, aber keinen awaitable-Typ zurückgibt, sollte ihr Name mit Begin, Start oder einem ähnlichen Verb beginnen, sodass eindeutig ist, dass diese Methode nicht das Ergebnis des Vorgangs zurückgibt.  

Die TAP-Methode gibt entweder System.Threading.Tasks.Task oder System.Threading.Tasks.Task<TResult> zurück. Das hängt davon ab, ob die entsprechende synchrone Methode „void“ oder einen TResult-Typ zurückgibt.

Die Parameter einer TAP-Methode sollten mit den Parametern der synchronen Entsprechung übereinstimmen und in der gleichen Reihenfolge bereitgestellt werden. Diese Regel gilt jedoch nicht für den out-Parameter und den ref-Parameter. Diese Parameter sollten vollständig vermieden werden. Alle Daten, die über einen out-Parameter oder einen ref-Parameter zurückgegeben werden, sollten stattdessen als Teil des von TResult zurückgegebenen Task<TResult> zurückgegeben werden, wobei für die Verwendung mehrerer Werte ein Tupel oder eine benutzerdefinierte Datenstruktur genutzt wird. Unter Umständen sollten Sie auch einen CancellationToken-Parameter hinzuzufügen, selbst dann, wenn die synchrone Entsprechung der TAP-Methode keinen anbietet.

Methoden, die ausschließlich zur Erstellung, Bearbeitung oder Kombination von Tasks dienen (wobei die asynchrone Verwendung der Methode im Methodennamen oder im Namen des Typs, zu dem die Methode gehört, angegeben wird), müssen nicht nach diesem Benennungsmuster benannt werden. Diese Methoden werden häufig als Combinators bezeichnet. Beispiele für Combinators umfassen WhenAll und WhenAny und werden im Abschnitt Verwenden der integrierten taskbasierten Combinators des Artikels Verwenden des taskbasierten asynchronen Musters erläutert.

Beispiele dafür, wie die TAP-Syntax sich von der Syntax in Legacyversionen des asynchronen Programmiermusters (beispielsweise dem asynchronen Programmiermodell (kurz APM) und dem ereignisbasierten asynchronen Muster (EAP)) unterscheidet, finden Sie unter Muster für die asynchrone Programmierung.

Initiieren eines asynchronen Vorgangs

Eine asynchrone Methode, die auf TAP basiert, kann eine kleine Menge an Arbeit synchron ausführen, beispielsweise das Überprüfen von Argumenten und das Initiieren des asynchronen Vorgangs, bevor sie die resultierende Aufgabe zurückgibt. Synchrone Arbeiten sollten auf ein Minimum beschränkt werden, damit die asynchrone Methode schnell zurückgeben kann. Gründe für eine schnelle Rückgabe sind u. a.:

  • Asynchrone Methoden können von den Threads der Benutzeroberfläche aufgerufen werden und jede synchrone Arbeit mit langer Laufzeit kann die Reaktionszeit der Anwendung beeinträchtigen.

  • Mehrere asynchrone Methoden können gleichzeitig ausgelöst werden. Daher kann jede Arbeit mit langer Laufzeit im synchronen Teil einer asynchronen Methode die Initiierung anderer asynchroner Operationen verzögern und so sie die Vorteile der Nebenläufigkeit verringern.

In einigen Fällen ist der Arbeitsaufwand, der erforderlich ist, um den Vorgang abzuschließen, kleiner als der Arbeitsaufwand, der erforderlich ist, um den Vorgang asynchron zu starten. Lesen aus einem Stream, bei dem der Lesevorgang durch Daten erfüllt werden kann, die bereits im Arbeitsspeicher gepuffert werden, ist ein Beispiel für ein solches Szenario. In solchen Fällen wird der Vorgang möglicherweise synchron abgeschlossen, und eine bereits abgeschlossene Aufgabe kann zurückgegeben werden.

Ausnahmen

Eine asynchrone Methode sollte nur Ausnahmen auslösen, die aufgrund eines Verwendungsfehlers beim Aufruf der asynchronen Methode ausgelöst werden. Verwendungsfehler sollten nie im Produktionscode auftreten. Verursacht die Übergabe eines Nullverweises (Nothing in Visual Basic) als Methodenargument beispielsweise einen Fehlerzustand (normalerweise dargestellt durch eine ArgumentNullException-Ausnahme), können Sie den Aufrufcode ändern, um sicherzustellen, dass Nullverweise nie übergeben werden. Für alle anderen Fehler sollten Ausnahmen, die beim Ausführen einer asynchronen Methode auftreten, der zurückgegebenen Aufgabe zugewiesen werden. Dies gilt auch dann, wenn die asynchrone Methode zufällig synchron abschließt, bevor die Aufgabe zurückgegeben wird. In der Regel enthält eine Aufgabe höchstens eine Ausnahme. Falls die Aufgabe jedoch mehrere Vorgänge darstellt (beispielsweise bei WhenAll), können einer einzelnen Aufgabe mehrere Ausnahmen zugeordnet werden.

Zielumgebung

Wenn Sie eine TAP-Methode implementieren, können Sie bestimmen, wo die asynchrone Ausführung auftritt. Sie können die Arbeitsauslastung im Threadpool ausführen, sie mithilfe einer asynchronen E/A implementieren (sodass sie für den Großteil der Ausführung des Vorgangs nicht an einen Thread gebunden ist), sie in einem bestimmten Thread (z. B. dem UI-Thread) ausführen lassen oder in einer beliebigen Anzahl von anderen potenziellen Kontexten verwenden. Es kann sogar vorkommen, dass eine TAP-Methode nichts ausführt und nur einen Task zurückgibt, der das Auftreten einer Bedingung an anderer Stelle im System darstellt (z.B. einen Task, der bei einer Datenstruktur in der Warteschlange eingehende Daten darstellt).

Die aufrufende Funktion der TAP-Methode blockiert möglicherweise den Wartevorgang für die TAP-Methode, indem synchron auf den resultierenden Task gewartet wird, oder führt eventuell zusätzlichen (Fortsetzungs-)Code aus, wenn der asynchrone Vorgang abgeschlossen ist. Der Ersteller des Fortsetzungscodes steuert, wo dieser Code ausgeführt wird. Sie können diesen Fortsetzungscode entweder explizit durch Methoden der Task-Klasse (z. B. ContinueWith) oder implizit mithilfe von Sprachunterstützung erstellen, die auf Fortsetzungen aufbaut (zum Beispiel await in C#, Await in Visual Basic, AwaitValue in F#).

Aufgabenstatus

Die Task-Klasse stellt einen Lebenszyklus für asynchrone Vorgänge bereit, und dieser Zyklus wird durch die TaskStatus-Enumeration dargestellt. Für die Unterstützung von Ausnahmefällen für Typen, die von Task und Task<TResult> abgeleitet werden, sowie für die Unterstützung der Trennung von Erstellung und Planung macht die Task-Klasse eine Start-Methode verfügbar. Von den öffentlichen Task-Konstruktoren erstellte Tasks werden als inaktive Tasks bezeichnet, da ihr Lebenszyklus im nicht geplanten Zustand Created beginnt und sie erst im geplanten Zustand sind, wenn Start für diese Instanzen aufgerufen wird.

Der Lebenszyklus aller anderen Aufgaben beginnt im aktiven Zustand. Dies bedeutet, dass die asynchronen Vorgänge, die sie darstellen, bereits initiiert wurden, und dass ihr Aufgabenzustand ein anderer Enumerationswert als TaskStatus.Created ist. Alle Aufgaben, die von TAP-Methoden zurückgegeben werden, müssen aktiviert sein. Wenn eine TAP-Methode intern den zurückzugebenden Task mit dem Konstruktor des Tasks instanziiert, muss die TAP-Methode vor dem Zurückgeben des Tasks Start im Task-Objekt aufrufen. Consumer einer TAP-Methode können davon ausgehen, dass die zurückgegebene Aufgabe aktiv ist, und sollten nicht versuchen, Start für einen von einer TAP-Methode zurückgegebenen Task aufzurufen. Der Aufruf von Start für eine aktive Aufgabe führt zu einer InvalidOperationException-Ausnahme.

Abbruch (optional)

Im TAP ist Abbruch sowohl für Implementierer asynchroner Methoden als auch für Consumer asynchroner Methoden optional. Wenn ein Vorgang einen Abbruch zulässt, macht er eine Überladung der asynchronen Methode verfügbar, die ein Abbruchtoken akzeptiert (CancellationToken-Instanz). Gemäß der Konvention lautet der Name des Parameters cancellationToken.

public Task ReadAsync(byte [] buffer, int offset, int count,
                      CancellationToken cancellationToken)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          cancellationToken As CancellationToken) _
                          As Task

Der asynchrone Vorgang überwacht dieses Token auf Abbruchanforderungen. Wenn es eine Abbruchanforderung empfängt, kann es diese Anforderung erfüllen und den Vorgang abbrechen. Wenn die Abbruchanforderung dazu führt, dass die Arbeit vorzeitig beendet wird, gibt die TAP-Methode eine Aufgabe zurück, die im Canceled-Zustand beendet wird. Es ist kein Ergebnis verfügbar und keine Ausnahme wird ausgelöst. Der Zustand Canceled gilt als endgültiger (abgeschlossener) Zustand einer Aufgabe, ebenso wie die Zustände Faulted und RanToCompletion. Wenn eine Aufgabe im Zustand Canceled ist, gibt ihre IsCompleted-Eigenschaft true zurück. Wenn eine Aufgabe im Canceled-Zustand abgeschlossen wird, werden alle mit der Aufgabe registrierten Fortsetzungen geplant oder ausgeführt, es sei denn, eine Fortsetzungsmöglichkeit wie NotOnCanceled wurde angegeben, um die Fortsetzung zu beenden. Die Ausführung von jedem Code, der mithilfe von Sprachfunktionen asynchron auf eine abgebrochene Aufgabe wartet, wird weiter ausgeführt, aber der Code empfängt eine OperationCanceledException-Ausnahme oder eine von ihr abgeleitete Ausnahme. Synchron blockierter Code, der durch Methoden wie Wait und WaitAll auf die Aufgabe wartet, wird ebenso weiter mit einer Ausnahme ausgeführt.

Wenn ein Abbruchtoken den Abbruch angefordert hat, bevor die TAP-Methode, die dieses Token akzeptiert, aufgerufen wurde, sollte die TAP-Methode eine Canceled-Aufgabe zurückgeben. Wenn jedoch der Abbruch während der Ausführung des asynchronen Vorgangs angefordert wird, muss der asynchrone Vorgang die Abbruchanforderung nicht erfüllen. Die zurückgegebene Aufgabe sollte nur dann im Canceled-Zustand enden, wenn der Vorgang aufgrund einer Abbruchanforderung beendet wird. Wird der Abbruch angefordert aber dennoch ein Ergebnis oder eine Ausnahme erzeugt, sollte die Aufgabe im Zustand RanToCompletion oder Faulted enden.

Für asynchrone Methoden, die als Erstes abgebrochen können werden sollen, müssen Sie keine Überladung bereitstellen, die kein Abbruchtoken akzeptiert. Für Methoden, die nicht abgebrochen werden können, sollten keine Überladungen bereitgestellt werden, die ein Abbruchstoken akzeptieren. So lässt sich leichter dem Aufrufer mitteilen, ob die Zielmethode tatsächlich abgebrochen werden kann. Consumercode, der keinen Abbruch wünscht, wird möglicherweise eine Methode aufrufen, die CancellationToken akzeptiert und None als den Argumentwert bereitstellen. None ist zu dem standardmäßigen CancellationToken funktional äquivalent.

Statusberichterstellung (optional)

Mehrere asynchrone Vorgänge profitieren von dem Bereitstellen von Statusbenachrichtigungen. Diese werden in der Regel verwendet, um eine Benutzeroberfläche mit Informationen zum Status der asynchronen Operation zu aktualisieren.

In TAP wird der Status wird durch eine IProgress<T>-Schnittstelle behandelt, die der asynchronen Methode als Parameter übergeben wird, der normalerweise progress genannt wird. Durch das Bereitstellen der Statusschnittstelle zum Zeitpunkt des Aufrufs der asynchronen Methode lassen sich leichter Racebedingungen vermeiden, die aus falscher Verwendung resultieren (d. h., wenn Ereignishandler, die nach dem Aufruf des Vorgangs nicht ordnungsgemäß registriert wurden, möglicherweise Updates verpassen). Vor allem unterstützt die Statusschnittstelle jedoch verschiedene Implementierungen des Status, die durch den verwendeten Code bestimmt werden. Beispielsweise soll der verwendete Code nur das neueste Statusupdate berücksichtigen oder alle Updates puffern oder für jedes Update eine Aktion aufrufen oder das Marshallen eines Aufrufs in einen bestimmten Thread steuern. All dies wird mithilfe einer anderen Implementierung der Schnittstelle erreicht, die an bestimmte Anforderungen des Consumers angepasst wird. Wie bei einem Abbruch sollten auch TAP-Implementierungen nur dann einen IProgress<T>-Parameter bereitstellen, wenn die API Statusbenachrichtigungen unterstützt.

Wenn beispielsweise die weiter oben in diesem Artikel beschriebene ReadAsync-Methode in der Lage ist, einen Zwischenstatus in Form der Anzahl von bisher gelesenen Bytes zu melden, könnte der Statusrückruf eine IProgress<T>-Schnittstelle sein:

public Task ReadAsync(byte[] buffer, int offset, int count,
                      IProgress<long> progress)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          progress As IProgress(Of Long)) As Task

Wenn eine FindFilesAsync-Methode eine Liste aller Dateien zurückgibt, die einem bestimmten Suchmuster entsprechen, kann der Statusrückruf einen geschätzten Prozentsatz der abgeschlossenen Arbeit sowie die aktuellen partiellen Ergebnisse bereitstellen. Diese Informationen können entweder als Tuple:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
            string pattern,
            IProgress<Tuple<double,
            ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

Oder mit einem API-spezifischen Datentyp bereitgestellt werden:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of FindFilesProgressInfo)) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

In letzterem Fall wird an den speziellen Datentyp für gewöhnlich das Suffix ProgressInfo angefügt werden.

Wenn TAP-Implementierungen Überladungen bereitstellen, die einen progress-Parameter akzeptieren, müssen sie null als Argument zulassen. In diesem Fall wird kein Status gemeldet. TAP-Implementierungen müssen den Status synchron an das Progress<T>-Objekt melden, was es der asynchronen Methode ermöglicht, den Status schnell zu melden. Außerdem kann der Statusconsumer ermitteln, wie und wo die Informationen am besten verarbeitet werden sollten. Zum Beispiel kann die Statusinstanz Rückrufe marshallen und Ereignisse auf einem aufgezeichneten Synchronisierungskontext auslösen.

IProgress<T>-Implementierungen

.NET stellt die Progress<T>-Klasse bereit, die IProgress<T> implementiert. Die Progress<T>-Klasse wird folgendermaßen deklariert:

public class Progress<T> : IProgress<T>  
{  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T>? ProgressChanged;  
}  

Eine Instanz von Progress<T> macht ein ProgressChanged-Ereignis verfügbar, das jedes Mal ausgelöst wird, wenn der asynchrone Vorgang ein Statusupdate meldet. Das ProgressChanged-Ereignis wird auf das SynchronizationContext-Objekt ausgelöst, das aufgezeichnet wurde, als die Progress<T>-Instanz instanziiert wurde. Wenn kein Synchronisierungskontext verfügbar war, wird ein Standardkontext, der auf den Threadpool abzielt, verwendet. Handler können mit diesem Ereignis registriert werden. Ein einzelner Handler kann auch dem Progress<T>-Konstruktor nach Wunsch bereitgestellt werden und verhält sich wie ein Ereignishandler für das ProgressChanged-Ereignis. Statusupdates werden asynchron ausgelöst, um den asynchronen Vorgang zu verzögern, während der Ereignishandler ausgeführt wird. Eine andere IProgress<T>-Implementierung kann festlegen, dass andere Semantik anzuwenden ist.

Auswählen der bereitzustellenden Überladungen

Verwendet eine TAP-Implementierung sowohl den optionalen Parameter CancellationToken als auch den optionalen Parameter IProgress<T>, kann dies möglicherweise bis zu vier Überladungen erfordern:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Viele TAP-Implementierungen bieten jedoch keine Abbruchs- oder Statusfunktionen und erfordern daher eine einzelne Methode:

public Task MethodNameAsync(…);  
Public MethodNameAsync(…) As Task  

Wenn eine TAP-Implementierung entweder Abbruch oder Status, jedoch nicht beides unterstützt, kann sie zwei Überladungen bereitstellen:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
  
// … or …  
  
public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task  
  
' … or …  
  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task  

Wenn eine TAP-Implementierung Abbruch und Status unterstützt, kann sie alle vier Überladungen verfügbar machen. Sie kann jedoch möglicherweise nur die Folgenden zwei bereitstellen:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Als Ausgleich für die zwei fehlenden Zwischenkombinationen können Entwickler None oder ein standardmäßiges CancellationToken für den cancellationToken-Parameter und null für den progress-Parameter übergeben.

Wenn Sie erwarten, dass jede Verwendung der TAP-Methode einen Abbruch oder Status unterstützt, können Sie die Überladungen weglassen, die den relevanten Parameter nicht akzeptieren.

Wenn Sie mehrere Überladungen verfügbar machen, um einen Abbruch oder Status als optional zu konfigurieren, sollte das Verhalten der Überladungen, die einen Abbruch oder Status nicht unterstützen, dem Verhalten einer Überladung entsprechen, die None für den Abbruch oder null für den Status an die Überladung übergibt, die diese Vorgänge jeweils unterstützt.

Titel Beschreibung
Muster für die asynchrone Programmierung Stellt die drei Muster zum Ausführen von asynchronen Vorgängen vor: das aufgabenbasierte asynchrone Muster (TAP), das asynchrone Programmiermodell (APM) und das ereignisbasierte asynchrone Muster (EAP).
Implementieren des aufgabenbasierten asynchronen Entwurfsmusters Beschreibt, wie Sie das aufgabenbasierte asynchrone Muster (Task-based Asynchronous Pattern, TAP) auf drei Arten implementieren können: mit C# und den Visual Basic-Compilern in Visual Studio, manuell oder mit einer Kombination von Compilermethoden und manuellen Methoden.
Nutzen des aufgabenbasierten asynchronen Musters Beschreibt, wie Sie Aufgaben und Rückrufe verwenden können, um eine Verzögerung ohne Blockierung zu erreichen.
Interoperabilität mit anderen asynchronen Mustern und Typen Beschreibt, wie das aufgabenbasierte asynchrone Muster (TAP) verwendet werden kann, um das asynchrone Programmiermodell (APM) und das ereignisbasierte asynchrone Muster (EAP) zu implementieren.