Asynchrone Programmierung in .NET 4.5 mit async und await

Sprachen  –  5 Kommentare

In der Vergangenheit war asynchrones, nicht blockierendes Programmieren unter .NET-Entwicklern nicht beliebt, denn Microsoft hatte dafür Verfahren in das .NET Framework eingebaut, die allesamt den Programmcode wesentlich verkomplizierten. Mit den Schlüsselwörtern async und await in C# 5.0 sowie Visual Basic 11.0 unterscheidet sich nun asynchroner Programmcode nicht mehr wesentlich von der stringenten synchronen Vorgehensweise.

Heute wird von Software erwartet, dass sie jederzeit auf Benutzereingaben reagiert, also auch dann, wenn sie gerade mit anderen Aufgaben beschäftigt ist. Diese Forderung ist nicht neu, aber gerade in Zeiten von Wischgesten und stagnierender Prozessorgeschwindigkeiten wichtiger denn je.

Microsoft hatte in .NET 1.0 Möglichkeiten implementiert, Multithreading (mit den Klassen im Namensraum System.Threading) und asynchrone Aufrufe (mit BeginInvoke() und EndInvoke() in der Delegate Klasse) zu nutzen. BeginInvoke() erzeugt dabei einen Thread, und EndInvoke() holt das Ergebnis später ab. Microsoft nannte das Asynchronous Programming Model (APM). In .NET 2.0 hatte das Unternehmen zusätzlich das Event-based Asynchronous Pattern (EAP) eingeführt, bei dem eine Klasse ein Ereignis auslöst, wenn die asynchrone Bearbeitung fertig ist. Ein Beispiel dazu ist die Methode DownloadStringAsync() mit dem zugehörigen Ereignis DownloadStringCompleted() in der Klasse System.Net.WebClient.

Mit .NET 4.0 gab es dann eine allgemeine Abstraktion von Threads in Form von Tasks mit der Task Parallel Library (TPL) sowie Parallel LINQ (PLINQ). TPL bietet eine gute Unterstützung für asynchrone Aufrufe, einschließlich der Festlegung von Abarbeitungsketten (Continuations) und des Abbruchs asynchroner Verarbeitungsprozesse (Cancellation). Dennoch verbleibt auch mit dieser Bibliothek für den Softwareentwickler die Notwendigkeit, den sequenziellen Programmcode erheblich umzustrukturieren, um eine asynchrone Programmierlogik zu realisieren.

Nach langer Zeit angekommen

C#-Schöpfer Anders Hejlberg sprach schon vor einigen Jahren auf der Professional Developers Conference (PDC) über die Idee von "future variables", bei denen ein Ergebnis eines Funktionsaufrufs im Programmcode zugewiesen wird, obwohl das Ergebnis erst irgendwann in der Zukunft eintrifft. Zur Umsetzung der Idee hat es bis 2012 (.NET 4.5) gedauert. Microsoft nennt das neue Konzept Task-based Asynchronous Pattern (TAP). Es basiert auf den Grundkonzepten der in .NET 4.0 eingeführten TPL, die Microsoft mit zwei neuen Schlüsselwörtern elegant in den Sprachen C# 5.0 und Visual Basic 11.0 verankert hat: async und await in C# beziehungsweise Async und Await in Visual Basic.

Eine Methode kann mit async deklarieren, dass sie plant, im Laufe ihrer Ausführung in einem eigenen Thread weiterzuarbeiten und die Kontrolle an den Aufrufer zurückzugeben. Eine solche asynchrone Methode muss dann ein Task-Objekt (aus der in .NET 4.0 eingeführten TPL) oder nichts (void) zurückliefern. await verwendet man in der asynchronen Methode an der Stelle, an der die asynchrone Ausführung beginnt. Dabei erhält der Aufruf der asynchronen Methode die Kontrolle zurück, während die asynchrone Methode auf die Fertigstellung des neuen Threads wartet und dann ihre eigene Ausführungsfolge nach dem await in diesem Thread fortsetzt.

Voraussetzung, damit wirklich eine asynchrone Ausführung stattfindet, ist, dass die bei await() aufgerufene Methode tatsächlich einen neuen Thread startet und mit await darauf wartet oder eine andere Methode mit await aufruft, die das tut. Weder await noch async starten von sich aus einen Thread. Sie dienen lediglich dazu, Auf- und Rückruf elegant im Programmcode zu verankern. await darf nur in mit async gekennzeichneten Methoden zum Einsatz kommen – das setzt der Sprachcompiler durch. Wenn der Entwickler async ohne await verwendet, warnt die Entwicklungsumgebung, es gibt aber keinen Compiler-Fehler (s. Abb. 1). Es sollte mindestens ein await in einer async-Methode geben.

Eine mit async gekennzeichnete Methode enthält kein await (Abb. 1).

Richtig asynchron formuliert, müsste die Methode aus Abbildung 1 so aussehen:

public async Task<int> DoWorkAsync()
{
Print("DoWorkAsync - Start");
var t = new Task<int>(() => DoWorkInternal());
t.Start();
Print("DoWorkAsync - Mitte");
var r = await t;
Print("DoWorkAsync - Ende");
return r;
}

Ein asynchrone Methode gibt den Rückgabewert nicht direkt zurück, sondern in ein Task-Objekt verpackt. Statt

public int DoWorkAsync(int p)

schreibt der Entwickler also:

public async Task<int> DoWorkAsync(int p)

Die asynchrone Methode kann den Rückgabewert aber ganz normal mit return liefern. Der Compiler kümmert sich um die Verpackung. Das Schlüsselwort await dient zudem dazu, den Rückgabewert einer solchen Methode zu entpacken. Der Entwickler schreibt entweder:

int r = await DoWorkAsync();

oder, wenn er zwischen Aufruf und Warten noch etwas erledigen will:

Task<int> t = DoWorkAsync();
Console.WriteLine("Aufruf gestartet...");
int r = await t;

Beim Einsatz von async und await sind aber weitere Regeln zu beachten. Einsprungpunkte (main()-Routinen) sowie Methoden mit ref- und out-Parametern dürfen nicht mit async gekennzeichnet werden. Ebenso dürfen solche Methoden nicht mit [SecurityCritical] und [SecuritySafeCritical] oder [MethodImpl(MethodImplOptions.Synchronized)] annotiert sein. await ist nicht in Getter- oder Setter-Routinen von Properties, in lock/SyncLock-Blöcken sowie in catch-, finally- und unsafe-Codeblöcken gestattet.

Das Schlüsselwort ist aber nicht auf die Verwendung mit Task-Objekten beschränkt. Ein Entwickler kann auch andere sogenannte "Awaitable"-Klassen entwickeln, die die Methode GetAwaiter() bereitstellen. Sie liefert dann ein "Awaiter"-Objekt, das die Schnittstelle System.Runtime.CompilerServices.INotifyCompletion realisiert. Auf diese Weise kann er auch Klassen, die mit den Pattern APM oder EAP arbeiten, mit einem eigenen "Awaitable"-Wrapper zum Einsatz mit async und await aufbereiten. await ist aber nicht beim Aufruf von Methoden möglich, die void zurückliefern. Das heißt, eine Methodensignatur wie

public async void DoWorkAsync()[/i] 

ist erlaubt, aber lässt sich nicht mit await aufrufen. Daher kommt async void typischerweise nur bei Ereignisbehandlungsroutinen zum Einsatz, die immer void liefern müssen.

Abbildung 2 zeigt ein Ablaufdiagramm für eine einfache asynchrone Befehlsfolge. Die synchron arbeitende MethodeA ruft die mit async gekennzeichnete MethodeB (Pfeil 1). Diese startet einen neuen Task, der die synchrone MethodeC ausführt (Pfeil 2a). Auf das Abarbeiten des Tasks wird mit await gewartet, wodurch der Aufrufer MethodeB die Kontrolle zurückerhält und der Rest der MethodeA ausgeführt wird (Pfeil 2b). Nach dem Ende von C wird der Rest der MethodeB abgearbeitet (Pfeil 3). Diese wird zunächst aus dem aus MethodeA stammenden Thread#1 (gelber Hintergrund) ausgeführt und dann – nach await – als Continuation von Thread#2, in dem die MethodeC erfolgt ist (roter Hintergrund).

Ablaufdiagramm eines asynchronen Aufrufs mit async und await (Abb. 2)