Asynchrone Programmierung in .NET 4.5 mit async und await

Seite 2: Beispiele

Inhaltsverzeichnis

Das folgende Beispiel zeigt einen asynchronen Datenbankzugriff mit Connection, Command und DataReader aus ADO.NET:

public static void run()
{
Console.WriteLine("Run() #1: Aufruf wird initiiert: Thread=" +
System.Threading.Thread.CurrentThread.ManagedThreadId);
ReadDataAsync();
Console.WriteLine("Run() #2: Aufruf ist erfolgt: Thread=" +
System.Threading.Thread.CurrentThread.ManagedThreadId);
}

static private async void ReadDataAsync()
{
Console.WriteLine("Beginn ReadDataAsync: Thread=" +
System.Threading.Thread.CurrentThread.ManagedThreadId);
// Datenbankverbindung asynchron aufbauen
SqlConnection conn = new SqlConnection(@"data source=
.\sqlexpress;initial catalog=WWWings6;integrated security=True;
MultipleActiveResultSets=True;App=ADONETClassic");
await conn.OpenAsync();
Console.WriteLine("Nach Open Async: Thread=" +
System.Threading.Thread.CurrentThread.ManagedThreadId);
// Daten asynchron abrufen
SqlCommand cmd = new SqlCommand("select top(10) * from flug", conn);
var reader = await cmd.ExecuteReaderAsync();
Console.WriteLine("Nach ExecuteReaderAsync: Thread=" +
System.Threading.Thread.CurrentThread.ManagedThreadId);
// Daten ausgeben
while (reader.Read())
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(reader["Zielort"]);
Console.ForegroundColor = ConsoleColor.Gray;
}

// Verbindung beenden
conn.Close();
Console.WriteLine("Ende ReadDataAsync: Thread=" +
System.Threading.Thread.CurrentThread.ManagedThreadId);
}

In diesen .NET-Klassen gibt es nun zusätzlich zu den bisherigen synchronen auch asynchrone Methoden. Im Beispiel ruft das Hauptprogramm Run() die selbst erstellte, mit async gekennzeichnete Methode ReadDataAsync() auf. Es ist dabei eine Konvention, aber keine Pflicht, dass der Name einer asynchronen Methode auf "async" endet. Die mit so kennzeichnete Methode muss als Rückgabewerte void oder Task<Typ> liefern.

In ihr kommen die von ADO.NET 4.5 bereitgestellten asynchronen Methoden OpenAsync() und ExecuteReaderAsync() zum Einsatz, die jeweils mit await aufgerufen werden. Die Anwendungsart ist hier eine .NET-Konsolenanwendung, in der man die Abfolge leicht visualisieren kann. Die Ausgabe der Thread-Nummern (ManagedThreadId) im langen Quellcode-Beispiel dient lediglich dazu, die asynchrone Ausführung in verschiedenen Threads zu belegen (s. Abb. 3).

Ausgabe der Konsolenanwendung als Beleg für die asynchrone Ausführung in den Threads (Abb. 3)

Führt ein Entwickler Programmcode wie den aus der Konsolen- in einer WPF-Anwendung (Windows Presentation Foundation) aus, wird er sich wundern, dass die ManagedThreadId immer die gleiche ist (s. unten Abb. 4). Dennoch ist die Ausführung asynchron, wie man an der Reihenfolge der Ausgabe sieht, denn "Aufruf ist erfolgt" steht vor "Nach Open Async". Dass Steuerelement-Interaktionen im gleichen Thread (dem UI-Thread) erfolgen, ist eine Leistung des im Hintergrund wirkenden DispatcherSynchronizationContext, der alle Continuations im UI-Thread abarbeitet.

Wenn der Entwickler allerdings auf traditionelle Weise die Eigenschaften von Steuerelementen festlegt, zum Beispiel einen TextBlock mit this.C_Status.Text += ausgabe + "\n"; befüllt, wird er verwundert feststellen, dass die Benutzeroberfläche dennoch blockiert. Nach einem Klick auf die Laden-Schaltfläche in Abbildung 4 kann der Benutzer an der Oberfläche bis zum Ende der Verarbeitung von C_Laden_Click nichts mehr machen, also auch nicht auf Abbrechen klicken. Mit der Blockade wäre die asynchrone Verarbeitung sinnlos.

Die Lösung besteht darin, das Steuerelement nicht direkt zu befüllen, sondern das WPF per Datenbindung erledigen zu lassen. Dazu gibt es in Listing 1 ein sogenanntes Dependency Property, das dann per

<TextBlock Name="C_Status" Height="200" Text="{Binding Status}" />

mit dem TextBlock-Steuerelement C_Status verbunden ist. Ein direkter Steuerelementzugriff ist dennoch im Code enthalten:

C_Daten.ItemsSource = dt.DefaultView; 

Der Zugriff führt allerdings nicht zur Blockade, selbst wenn der Entwickler ihn immer wieder wiederholt.

Die automatische Lenkung aller Continuations in den UI-Thread kann er mit der Übergabe von false an die Methode ConfigureAwait() ausschalten, zum Beispiel

await ReadDataAsync(cts.Token).ConfigureAwait(false)

Dann aber muss er sich selbst darum kümmern, alle Zugriffe auf die Oberfläche wieder auf den UI-Thread zu bringen (z. B. mit dem WPF Dispatcher). Denn WPF quittiert sonst alle Versuche, aus einem anderen als dem UI-Thread auf die Oberfläche zuzugreifen, mit dem Laufzeitfehler: "Der aufrufende Thread kann nicht auf dieses Objekt zugreifen, da sich das Objekt im Besitz eines anderen Threads befindet."

Ausgabe einer mit dem obigen langen Quellcode-Beispiel korrespondieren WPF-Anwendung (Abb. 4)

Listing 1 zeigt die Realisierung des Anwendungscodes für die WPF-Anwendung einschließlich Unterstützung für den Abbruch der Aktion. Dafür erzeugt der Aufrufer mit der Klasse CancellationTokenSource ein CancellationToken-Objekt, das der async-Methode übergeben wird. Der Aufruf kann dann über die Instanz von CancellationTokenSource den Abbruch signalisieren. Der aufgerufene, asynchron ausgeführte Programmcode erfährt über IsCancellationRequested im CancellationToken-Objekt, ob ein Abbruch erwartet wird. Er kann, muss aber darauf nicht reagieren.