Der kürzlich veröffentlichte Blogbeitrag Schnelle und flüssige Apps mit Asynchronität in der Windows-Runtime enthält Beispiele dazu, wie das await-Schlüsselwort in C# und Visual Basic von Entwicklern verwendet wird, um asynchrone WinRT-Vorgänge zu nutzen und gleichzeitig für eine gute Ablaufsteuerung zu sorgen.

In diesem anknüpfenden Beitrag möchte ich genauer darauf eingehen, wie await mit WinRT funktioniert. Diese Kenntnisse vereinfachen das Durchdenken von Code, in dem awaitverwendet wird, und erleichtert Ihnen so das Schreiben von besseren Apps im Metro-Stil.

Beginnen wir damit, uns das Schreiben von Code ohne await vorzustellen.

Wiederholung der Grundlagen

Die gesamte Asynchronität in der WinRT entstammt einer einzigen Schnittstelle: IAsyncInfo.

public interface IAsyncInfo
{
AsyncStatus Status { get; }
HResult ErrorCode { get; }
uint Id { get; }

void Cancel();
void Close();
}

Diese Schnittstelle wird in jedem asynchronen WinRT-Vorgang implementiert, um die erforderliche Grundfunktionalität für asynchrone Vorgänge bereitzustellen, die Identität und den Status abzufragen sowie den Abbruch anzufordern. Dieser speziellen Schnittstelle fehlt jedoch der wohl entscheidendste Aspekt eines asynchronen Vorgangs: ein Callback, der einen Listener benachrichtigt, wenn ein Vorgang abgeschlossen ist. Diese Funktion wurde absichtlich in vier weitere Schnittstellen aufgeteilt, die alle IAsyncInfo erfordern. In jedem asynchronen WinRT-Vorgang wird eine dieser vier Schnittstellen implementiert:

public interface IAsyncAction : IAsyncInfo
{
AsyncActionCompletedHandler Completed { get; set; }
void GetResults();
}

public interface IAsyncOperation<TResult> : IAsyncInfo
{
AsyncOperationCompletedHandler<TResult> Completed { get; set; }
TResult GetResults();
}

public interface IAsyncActionWithProgress<TProgress> : IAsyncInfo
{
AsyncActionWithProgressCompletedHandler<TProgress> Completed { get; set; }
AsyncActionProgressHandler<TProgress> Progress { get; set; }
void GetResults();
}

public interface IAsyncOperationWithProgress<TResult, TProgress> : IAsyncInfo
{
AsyncOperationWithProgressCompletedHandler<TResult, TProgress> Completed { get; set; }
AsyncOperationProgressHandler<TResult, TProgress> Progress { get; set; }
TResult GetResults();
}

Diese vier Schnittstellen unterstützen alle Kombinationen von „mit oder ohne Ergebnis“ und „mit oder ohne Statusberichterstellung“. Alle diese Schnittstellen machen eine Completed-Eigenschaft verfügbar, die auf einen Delegaten festgelegt werden kann, der beim Vorgangsabschluss aufgerufen wird. Sie können den Delegaten nur einmal festlegen. Wenn die Festlegung nach dem Abschluss des Vorgangs erfolgt, wird der Delegat unmittelbar geplant oder aufgerufen, wobei die Priorität des Vorgangsabschlusses und der Zuweisung des Delegaten von der Implementierung abhängig ist.

Angenommen, ich möchte eine App im Metro-Stil mit einer XAML-Schaltfläche so implementieren, dass durch Klicken auf die Schaltfläche eine Aufgabe in die Warteschlange für den WinRT-Threadpool eingefügt wird, um einen Vorgang mit hoher Rechenleistung auszuführen. Wenn die Aufgabe abgeschlossen ist, wird der Inhalt der Schaltfläche mit dem Ergebnis des Vorgangs aktualisiert.

Wie lässt sich dies implementieren? Die ThreadPool-Klasse der WinRT macht eine Methode verfügbar, um Aufgaben im Pool asynchron auszuführen:

public static IAsyncAction RunAsync(WorkItemHandler handler);

Mithilfe dieser Methode kann der rechenintensive Vorgang in die Warteschlange eingefügt werden, um zu verhindern, dass der Benutzeroberflächen-Thread während der Ausführung blockiert wird:

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
}

Die Aufgabe wurde vom Benutzeroberflächen-Thread in den Pool ausgelagert, aber woher wissen wir, wann sie abgeschlossen ist? RunAsync gibt eine IAsyncAction zurück, sodass zum Empfangen dieser Benachrichtigung ein Completion-Handler eingesetzt und als Reaktion die Fortsetzung ausgeführt werden kann:

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
btnDoWork.Content = result.ToString(); // bug!
};
}

Wenn nun der asynchrone Vorgang in der Warteschlange des ThreadPools abgeschlossen ist, wird der Completed-Handler aufgerufen und versucht, das Ergebnis in der Schaltfläche zu speichern. Unglücklicherweise ist dies zurzeit fehlerhaft. Es ist unwahrscheinlich, dass der Completed-Handler im Benutzeroberflächen-Thread aufgerufen wird, der Handler muss jedoch zum Ändern von btnDoWork.Content im Benutzeroberflächen-Thread ausgeführt werden (andernfalls wird eine Ausnahme mit dem Fehlercode RPC_E_WRONG_THREAD ausgelöst). Um dies zu umgehen, kann das der Benutzeroberfläche zugeordnete CoreDispatcher-Objekt verwendet werden, damit der Aufruf wieder an die erforderliche Stelle gemarshallt wird:

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); });
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, delegate
{
btnDoWork.Content = result.ToString();
});
};
}

Dies funktioniert jetzt. Was aber geschieht, wenn die Compute-Methode eine Ausnahme auslöst, oder jemand Cancel für die von ThreadPool.RunAsync zurückgegebene IAsyncAction aufruft? Der Completed-Handler muss damit umgehen können, dass die IAsyncAction in einem von drei abschließenden Zuständen enden kann – Completed, Error oder Canceled:

private void btnDoWork_Click(object sender, RoutedEventArgs e)
{
int result = 0;
var op = ThreadPool.RunAsync(delegate { result = Compute(); })
op.Completed = delegate(IAsyncAction asyncAction, AsyncStatus asyncStatus)
{
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, delegate
{
switch (asyncStatus)
{
case AsyncStatus.Completed:
btnDoWork.Content = result.ToString();
break;
case AsyncStatus.Error:
btnDoWork.Content = asyncAction.ErrorCode.Message;
break;
case AsyncStatus.Canceled:
btnDoWork.Content = "A task was canceled";
break;
}
});
};
}

Dies ist eine ziemliche Menge Code für das Verarbeiten eines einzigen asynchronen Aufrufs. Stellen Sie sich vor, wie dies aussähe, wenn viele asynchrone Vorgänge nacheinander ausgeführt werden müssten. Wäre es nicht praktisch, stattdessen Code wie den folgenden schreiben zu können?

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
try
{
int result = 0;
await ThreadPool.RunAsync(delegate { result = Compute(); });
btnDoWork.Content = result.ToString();
}
catch (Exception exc) { btnDoWork.Content = exc.Message; }
}

Dieser Code verhält sich genau wie der vorherige Code. Wir müssen uns jedoch nicht manuell um Abschluss-Callbacks kümmern. Auch das manuelle Marshalling in den Benutzeroberflächen-Thread entfällt. Der Abschlussstatus muss nicht explizit überprüft werden. Zudem muss die Ablaufsteuerung nicht umgekehrt werden, d. h. es können nun problemlos weitere Vorgänge hinzugefügt werden, beispielsweise um mehrfache Berechnungen und Benutzeroberflächenupdates in einer Schleife vorzunehmen:

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
try
{
for(int i=0; i<5; i++)
{
int result = 0;
await ThreadPool.RunAsync(delegate { result = Compute(); });
btnDoWork.Content = result.ToString();

}
}
catch (Exception exc) { btnDoWork.Content = exc.Message; }
}

Denken Sie nur kurz an den Code, den Sie hätten schreiben müssen, wenn Sie IAsyncAction manuell verwenden müssten. Dies ist der Zauber der neuen async- und await-Schlüsselwörter in C# und Visual Basic. Die gute Nachricht ist, dass Sie tatsächlich genau diesen Code schreiben können, und es in Wirklichkeit keine Zauberei ist. Im Rest dieses Beitrags erkunden wir die genaue Funktionsweise hinter den Kulissen.

Compilertransformationen

Das Kennzeichnen einer Methode mit dem async-Schlüsselwort bewirkt, dass der C#- oder Visual Basic-Compiler die Implementierung der Methode mithilfe eines Zustandsautomaten neu schreibt. Durch diesen Zustandsautomaten kann der Compiler Punkte in die Methode einfügen, an denen die Ausführung der Methode angehalten und fortgesetzt werden kann, ohne einen Thread zu blockieren. Diese Punkte werden nicht zufällig eingefügt. Sie werden nur dort eingefügt, wo Sie ausdrücklich das await-Schlüsselwort verwenden:

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
...
await something; // <-- potential method suspension point
...
}

Während Sie auf einen asynchronen Vorgang warten, der noch nicht abgeschlossen ist, stellt der vom Compiler generierte Code sicher, dass alle mit dem Status der Methode zusammenhängenden Elemente (z. B. örtliche Variablen) zu einem Paket zusammengefasst und im Heap beibehalten wird. Anschließend wird die Funktion an den Aufrufer zurückgegeben, sodass der Thread, auf dem sie ausgeführt wurde, für andere Aufgaben freigegeben wird. Wenn dann der asynchrone Vorgang, auf den gewartet wurde, abgeschlossen ist, wird die Methode unter Verwendung des beibehaltenen Status weiter ausgeführt.

Jeder Typ, der das Await-Muster verfügbar macht, kann abgewartet werden. Das Muster besteht in erster Linie darin, eine GetAwaiter-Methode verfügbar zu machen, die einen Typ zurückgibt, der IsCompleted-, OnCompleted- und GetResult-Member bereitstellt. Beispiel:

await something;

Der Compiler generiert Code, der diese Member verwendet, um zu überprüfen, ob das Objekt bereits abgeschlossen ist (mittels IsCompleted). Ist dies nicht der Fall, erfolgt eine Fortsetzung (mittels OnCompleted), die einen Callback zurückgibt, um die Ausführung fortzusetzen, wenn der Task abgeschlossen wird. Nachdem der Vorgang abgeschlossen ist, werden alle Ausnahmen aus dem Vorgang verteilt und/oder es wird ein Ergebnis zurückgegeben (mittels GetResult). Wenn Sie also diesen Code schreiben:

private async void btnDoWork_Click(object sender, RoutedEventArgs e)
{
...
await ThreadPool.RunAsync(delegate { result = Compute(); });
...
}

übersetzt der Compiler es in Code, der diesem ähnelt:

private class btnDoWork_ClickStateMachine : IAsyncStateMachine
{
// Member fields for preserving locals and other necessary state
int $state;
TaskAwaiter<string> $awaiter;
int result;
...
// Method that moves to the next state in the state machine
public void MoveNext()
{
// Jump table to get back to the right statement upon resumption
switch (this.$state)
{
...
case 2: goto Label2;
...
}
...
// Expansion of await ...;
var tmp1 = ThreadPool.RunAsync(delegate { this.result = Compute(); });
this.$awaiter = tmp1.GetAwaiter();
if (!this.$awaiter.IsCompleted)
{
this.$state = 2;
this.$awaiter.OnCompleted(MoveNext);
return;
Label2:
}
this.$awaiter.GetResult();
...
}
}

Für die btnDoWork_Click-Methode generiert der Compiler eine Zustandsautomatenklasse, die eine MoveNext-Methode beinhaltet. Jeder Aufruf von MoveNext nimmt die Ausführung der btnDoWork_Click-Methode wieder auf, bis diese den nächsten Await-Punkt für ein noch nicht abgeschlossenes Element oder das Ende der Methode erreicht, je nachdem, was zuerst eintritt. Wenn der vom Compiler generierte Code auf eine noch nicht abgeschlossene Instanz trifft, auf die gewartet wird, markiert er den aktuellen Punkt mit einer Zustandsvariablen, plant die Ausführung der Methode so, dass diese nach Abschluss der erwarteten Instanz fortgesetzt wird, und kehrt dann zurück. Wenn die Instanz, auf die gewartet wird, abgeschlossen ist, wird die MoveNext-Methode erneut aufgerufen und springt an den Punkt in der Methode, wo die Ausführung zuvor angehalten wurde.

Der Compiler berücksichtigt nicht, dass hier auf eine IAsyncAction gewartet wird. Das einzig Bedeutsame ist die Verfügbarkeit des richtige Musters, an das anknüpft werden kann. Sie haben natürlich gesehen, wie die IAsyncAction-Schnittstelle aussieht und dass sie keine GetAwaiter-Methode enthält, wie vom Compiler erwartet. Wie kann dieser Code also erfolgreich kompiliert und ausgeführt werden? Zur Erläuterung ist es hilfreich, wenn Sie sich mit den zunächst mit den .NET-Typen Task und Task<TResult> (die Hauptdarstellung von asynchronen Vorgängen im Framework) und deren Zusammenhang mit der Await-Funktion vertraut machen.

Konvertierung in Tasks

.NET Framework 4.5 beinhaltet alle erforderlichen Typen und Methoden, um abgewartete Task- und Task<TResult>-Instanzen (Task<TResult> wird aus Task abgeleitet) zu unterstützen. Task und Task<TResult> machen beide GetAwaiter-Instanzmethoden verfügbar, die jeweils TaskAwaiter- und TaskAwaiter<TResult>-Typen zurückgeben. Diese wiederum machen die IsCompleted-, OnCompleted- und GetResult-Member verfügbar, die für die C#- und Visual Basic-Compiler erforderlich sind. IsCompleted gibt einen booleschen Wert zurück, der angibt, ob die Ausführung des Tasks zum Zeitpunkt des Zugriffs auf die Eigenschaft abgeschlossen ist. OnCompleted verbindet den Task mit einem Fortsetzungsdelegaten, der aufgerufen wird, wenn der Task abgeschlossen ist. (Ist der Task bereits abgeschlossen, wenn OnCompleted aufgerufen wird, wird der Fortsetzungsdelegat asynchron zur Ausführung geplant.) GetResult gibt das Ergebnis des Tasks zurück, wenn dieser mit dem Status TaskStatus.RanToCompletion beendet wurde (für den nicht generischen Task-Typ wird „void“ zurückgegeben), oder löst eine OperationCanceledException aus, wenn der Task mit dem Status TaskStatus.Canceled beendet wurde, bzw. löst die Ausnahme aus, durch die der Task beendet wurde, falls der Status hierbei TaskStatus.Faulted war.

Wenn das Erwarten durch einen benutzerdefinierten Typ unterstützt werden soll, stehen hierfür zwei primäre Möglichkeiten zur Verfügung. Eine Möglichkeit besteht darin, das gesamte „await“-Muster manuell für den „awaitable“-Typ zu implementieren, indem wir eine GetAwaiter-Methode bereitstellen, mit deren Hilfe die Fortsetzungen sowie die Ausnahmeverteilung und dergleichen erfolgen. Zweitens lässt sich auch die Möglichkeit implementieren, den benutzerdefinierten Typ in einen Task zu konvertieren und anschließend die integrierte Unterstützung für das Erwarten von Tasks zu verwenden, um den spezifischen Typ zu erwarten. Sehen wir uns letzteren Ansatz ein wenig genauer an.

.NET Framework enthält den Typ TaskCompletionSource<TResult>, mit dem sich Konvertierungen dieser Art einfach ausführen lassen. TaskCompletionSource<TResult> erstellt ein Task<TResult>-Objekt und stellt die Methoden SetResult, SetException und SetCanceled bereit, mit denen Sie unmittelbar steuern können, wann und bei welchem Status der entsprechende Task abgeschlossen wird. Sie können TaskCompletionSource<TResult> also als eine Art Shim oder Proxy verwenden, um einen anderen asynchronen Vorgang – beispielsweise einen asynchronen WinRT-Vorgang – darzustellen.

Nehmen wir für einen Moment an, dass Ihnen noch nicht bekannt war, dass auf WinRT-Vorgänge direkt gewartet werden kann. Wie könnten Sie dies dann erreichen?

IAsyncOperation<string> op = SomeMethodAsync();
string result = await ...; // need something to await

Sie könnten einen TaskCompletionSource<TResult>-Typ erstellen, diesen als Proxy verwenden, um den asynchronen WinRT-Vorgang darzustellen, und dann den entsprechenden Task erwarten. Versuchen wir es einfach. Zunächst müssen wir hierfür einen TaskCompletionSource<TResult>-Typ instanziieren, um den Task zu erwarten:

IAsyncOperation<string> op = SomeMethodAsync();
var tcs = new TaskCompletionSource<TResult>();
...
string result = await tcs.Task;

Anschließend müssen wir, wie im vorherigen Beispiel für die manuelle Verwendung der Completed-Handler des asynchronen WinRT-Vorgangs gezeigt, ein Callback mit dem asynchronen Vorgang verknüpfen, um feststellen zu können, wann dieser abgeschlossen wird:

IAsyncOperation<string> op = SomeMethodAsync();

var tcs = new TaskCompletionSource<TResult>();

op.Completed = delegate
{
...
};
string result = await tcs.Task;

Danach müssen wir in diesem Callback den Abschlussstatus von IAsyncOperation<TResult> an den Task übertragen:

IAsyncOperation<string> op = SomeMethodAsync();
var tcs = new TaskCompletionSource<TResult>();
op.Completed = delegate
{
switch(operation.Status)
{
AsyncStatus.Completed:
tcs.SetResult(operation.GetResults());
break;
AsyncStatus.Error:
tcs.SetException(operation.ErrorCode);
break;
AsyncStatus.Canceled:
tcs.SetCanceled();
break;
}
};
string result = await tcs.Task;

So einfach ist das. Mit asynchronen WinRT-Vorgängen wird sichergestellt, das der Completed-Handler selbst dann richtig aufgerufen wird, wenn dieser erst nach Abschluss des Vorgangs registriert wird. Daher ist es nicht erforderlich, den Handler vor dem Abschluss des Vorgangs gesondert zu registrieren. Außerdem übergeben asynchrone WinRT-Vorgänge nach Abschluss des Vorgangs auch den Verweis an den Completed-Handler. Daher muss Completed beim Aufrufen des Handlers auch nicht gesondert auf NULL festgelegt werden. Darüber hinaus handelt es sich beim Completed-Handler um einen Handler mit einmaliger Festlegung, d. h. beim zweiten Versuch einer Festlegung wird eine Fehlermeldung ausgegeben.

Mit diesem Ansatz besteht eine eindeutige Zuordnung zwischen dem Status, mit dem der asynchrone WinRT-Vorgang abgeschlossen wird, und dem Status, in dem der darstellende Task abgeschlossen wird:

Abschließender AsyncStatus

Konvertierung in TaskStatus

Der, wenn erwartet...

Completed

RanToCompletion

Das Ergebnis des Vorgangs zurückgibt (oder „void“)

Error

Faulted

Die Ausnahme des fehlgeschlagenen Vorgangs auslöst

Canceled

Canceled

Eine OperationCanceledException auslöst

Selbstverständlich wäre es rasch lästig, wenn Sie den für diesen einen await vorgestellten Codebaustein jedes Mal schreiben müssten, wenn ein asynchroner WinRT-Vorgang erwartet werden soll. Doch als gute Programmierer können wir diesen Codebaustein in eine Methode kapseln, die sich beliebig oft verwenden lässt. Verwenden wir hierzu eine Erweiterungsmethode, mit der der asynchrone WinRT-Vorgang in einen Task konvertiert wird:

public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> operation)
{
var tcs = new TaskCompletionSource<TResult>();
operation.Completed = delegate
{
switch(operation.Status)
{
AsyncStatus.Completed:
tcs.SetResult(operation.GetResults());
break;
AsyncStatus.Error:
tcs.SetException(operation.ErrorCode);
break;
AsyncStatus.Canceled:
tcs.SetCanceled();
break;
}
};
return tcs.Task;
}

Mit dieser Erweiterungsmethode lässt sich beispielsweise der folgende Code schreiben:

IAsyncOperation<string> op = SomeMethodAsync();
string result = await op.AsTask();

Oder ganz einfach auch:

string result = await SomeMethodAsync().AsTask();

Viel besser. Selbstverständlich ist diese AsTask-Funktion für alle Entwickler von großem Nutzen, die die WinRT im Zusammenhang mit C# und Visual Basic verwenden. Daher müssen Sie auch keine eigene Implementierung schreiben: In .NET 4.5 sind derartige Methoden bereits implementiert. Die System.Runtime.WindowsRuntime.dll-Assembly enthält diese Erweiterungsmethoden für die asynchronen WinRT-Schnittstellen:

namespace System
{
public static class WindowsRuntimeSystemExtensions
{
// IAsyncAction

public static Task AsTask(
this IAsyncAction source);
public static Task AsTask(
this IAsyncAction source,
CancellationToken cancellationToken);

// IAsyncActionWithProgress

public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
IProgress<TProgress> progress);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
CancellationToken cancellationToken);
public static Task AsTask<TProgress>(
this IAsyncActionWithProgress<TProgress> source,
CancellationToken cancellationToken,
IProgress<TProgress> progress);

// IAsyncOperation

public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> source);
public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> source,
CancellationToken cancellationToken);

// IAsyncOperationWithProgress

public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
IProgress<TProgress> progress);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
CancellationToken cancellationToken);
public static Task<TResult> AsTask<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source,
CancellationToken cancellationToken,
IProgress<TProgress> progress);

...
}
}

Jede der vier Schnittstellen verfügt über eine parameterlose AsTask-Überladung, vergleichbar mit der, die wir soeben vollständig neu erstellt haben. Zusätzlich verfügen sämtliche Schnittstellen über eine Überladung, die ein CancellationToken akzeptiert. Dieses Token wird in .NET in der Regel dazu verwendet, einen zusammensetzbaren und kooperativen Abbruch bereitzustellen. Sie übergeben ein Token an alle asynchronen Vorgänge. Wenn anschließend ein Abbruch angefordert wird, gilt dieser für sämtliche dieser asynchronen Vorgänge. Wie könnten wir (nur zur Verdeutlichung, da Sie bereits wissen, dass eine solche API schon verfügbar ist), eine derartige AsTask(CancellationToken)-Überladung selbst erstellen? CancellationToken stellt eine Register-Methode bereit, die einen Delegat akzeptiert, der bei einer Abbruchanforderung aufgerufen wird. Wir können einfach einen Delegat bereitstellen, der bei einer Abbruchaufforderung im IAsyncInfo-Objekt Cancel aufruft:

public static Task<TResult> AsTask<TResult>(
this IAsyncOperation<TResult> operation,
CancellationToken cancellationToken
{
using(cancellationToken.Register(() => operation.Cancel()))
return await operation.AsTask();
}

Auch wenn die Implementierung in .NET 4.5 der hier beschriebenen nicht genau entspricht, handelt es sich von der Logik her um dasselbe Prinzip.

Für IAsyncActionWithProgress<TProgress> und IAsyncOperationWithProgress<TResult,TProgress> stehen ebenfalls Überladungen zur Verfügung, die IProgress<TProgress> akzeptieren. Bei IProgress<T> handelt es sich um eine .NET-Schnittstelle, die von Methoden für die Rückgabe des Fortschritts verwendet werden kann. Die AsTask-Methode verwendet einfach einen Delegat für die Progress-Eigenschaft des asynchronen WinRT-Vorgangs, um die Fortschrittsinformationen mithilfe der Eigenschaft an IProgress weiterzuleiten. Nochmals, nur als Beispiel für eine manuelle Implementierung:

public static Task<TResult> AsTask<TResult,TProgress>(
this IAsyncOperationWithProgress<TResult> operation,
IProgress<TProgress> progress
{
operation.Progress += (_,p) => progress.Report(p);
return operation.AsTask();
}

Direktes Erwarten von asynchronen WinRT-Vorgängen

Wir haben bereits gesehen, wie sich Tasks für die Darstellung eines asynchronen WinRT-Vorgangs so erstellen lassen, dass wir diese Tasks erwarten können. Doch wie lassen sich WinRT-Vorgänge direkt erwarten? Anders ausgedrückt kann ohne Weiteres Folgendes geschrieben werden:

await SomeMethodAsync().AsTask();

Doch wäre es in Fällen, in denen auf ein CancellationToken oder IProgress<T> verzichtet werden kann, nicht praktisch, das Schreiben von Code für den Aufruf von AsTask vollständig zu vermeiden?

await SomeMethodAsync();

Wie bereits zu Anfang dieses Beitrags gezeigt, ist dies selbstverständlich möglich. Erinnern Sie sich, dass der Compiler eine GetAwaiter-Methode erwartet, die einen geeigneten „awaiter“-Typ zurückgibt? Der bereits erwähnte WindowsRuntimeSystemExtensions-Typ in der System.Runtime.WindowsRuntime.dll enthält eine ebensolche GetAwaiter-Erweiterungsmethode für die vier asynchronen WinRT-Schnittstellen:

namespace System
{
public static class WindowsRuntimeSystemExtensions
{
...
public static TaskAwaiter GetAwaiter(
this IAsyncAction source);
public static TaskAwaiter<TResult> GetAwaiter<TResult>(
this IAsyncOperation<TResult> source);
public static TaskAwaiter GetAwaiter<TProgress>(
this IAsyncActionWithProgress<TProgress> source);
public static TaskAwaiter<TResult> GetAwaiter<TResult, TProgress>(
this IAsyncOperationWithProgress<TResult, TProgress> source);
}
}

Beachten Sie den Rückgabetyp jeder dieser vier Methoden: TaskAwaiter bzw. TaskAwaiter<TResult>. Jede diese vier Methoden nutzt die integrierten TaskAwaiters im Framework. Da Sie AsTask bereits kennen, können Sie sich wahrscheinlich denken, wie diese implementiert werden. Die tatsächliche Implementierung im Framework entspricht ziemlich genau dieser:

public static TaskAwaiter GetAwaiter(
this IAsyncAction source)
{
return source.AsTask().GetAwaiter();
}

Dies bedeutet, dass beide Zeilen exakt dasselbe Verhalten auslösen:

await SomeMethodAsync().AsTask();
await SomeMethodAsync();

Benutzerdefiniertes „await“-Verhalten

Wie bereits erwähnt, stellen TaskAwaiter und TaskAwaiter<TResult> alle Member bereit, die der Compiler für einen Awaiter benötigt:

bool IsCompleted { get; }
void OnCompleted(Action continuation);
TResult GetResult(); //returns void on TaskAwaiter

Der interessanteste Member ist in diesem Fall OnCompleted, da mit diesem der Fortsetzungsdelegat aufgerufen wird, wenn der erwartete Vorgang abgeschlossen ist. OnCompleted stellt ein spezielles Marshalling-Verhalten bereit, mit dem sichergestellt wird, dass der Fortsetzungsdelegat an der richtigen Stelle ausgeführt wird.

Beim Aufrufen des Awaiters des Tasks OnCompleted wird der aktuelle SynchronizationContext berücksichtigt. Hierbei handelt es sich um eine abstrakte Darstellung der Umgebung, in der der Code ausgeführt wird. Im Benutzeroberflächen-Thread einer App im Metro-Stil gibt SynchronizationContext.Current eine Instanz des internen WinRTSynchronizationContext-Typs zurück. SynchronizationContext stellt eine virtuelle Post-Methode bereit, die einen Delegat akzeptiert und diesen Delegat kontextabhängig an einer geeigneten Stelle ausführt. WinRTSynchronizationContext umschließt einen CoreDispatcher und verwendet dessen RunAsync, um den Delegaten zurück im Benutzeroberflächen-Thread asynchron aufzurufen (wie bereits zuvor in diesem Beitrag manuell erfolgt). Beim Abschluss des erwarteten Tasks wird der an OnCompleted übergebene Delegat zur Ausführung per Post an den aufgezeichneten SynchronizationContext übertragen, der beim Aufrufen von OnCompleted aktuell war. So können Sie Code schreiben, der await in der Benutzeroberflächen-Logik verwendet, ohne sich Gedanken über das Marshalling zurück an den richtigen Thread machen zu müssen: Dies übernimmt der Awaiter des Tasks.

Selbstverständlich gibt es zahlreiche Situationen, in denen Sie dieses Marshalling-Standardverhalten nicht verwenden möchten. Dies ist beispielsweise häufig bei Bibliotheken der Fall: Zahlreiche Bibliotheken führen nehmen keine Änderungen an Benutzeroberflächen-Steuerelementen oder am jeweiligen Thread vor, in dem sie ausgeführt werden. Daher ist es hinsichtlich der Leistung nützlich, in der Lage zu sein, den mit Thread-übergreifendem Marshalling verbundenen Mehraufwand zu vermeiden. Task und Task<TResult> stellen ConfigureAwait-Methoden bereit, mit denen sich Code so anpassen lässt, dass dieses standardmäßige Marshalling-Verhalten deaktiviert wird. ConfigureAwait akzeptiert einen booleschen continueOnCapturedContext-Parameter: Bei der Übergabe von „true“ wird das Standardverhalten verwendet. Die Übergabe von „false“ bedeutet, dass das System das Aufrufen des Delegaten nicht per Marshalling zurück an den ursprünglichen Kontext erzwingen muss und den Delegaten stattdessen an einer vom System bestimmten Stelle ausführen kann.

Dies vorausgesetzt, können Sie, wenn Sie auf einen WinRT-Vorgang warten möchten, ohne die übrige Ausführung zurück im Benutzeroberflächen-Thread zu erzwingen, statt

await SomeMethodAsync();

oder

await SomeMethodAsync().AsTask();

zu schreiben

await SomeMethodAsync().AsTask()
.ConfigureAwait(continueOnCapturedContext:false);

oder einfach Folgendes verwenden:

await SomeMethodAsync().AsTask().ConfigureAwait(false);

Wann sollte „AsTask“ verwendet werden?

Wenn Sie einfach einen asynchronen WinRT-Vorgang aufrufen und auf dessen Abschluss warten möchten, ist der einfachste und funktionalste Ansatz das direkte Erwarten des asynchronen WinRT-Vorgangs:

await SomeMethodAsync();

Wenn Sie jedoch weitere Steuerungsmöglichkeiten nutzen möchten, müssen Sie AsTask verwenden. Einigen Fälle, bei denen sich dies empfiehlt, haben Sie bereits kennengelernt:

  • Unterstützen des Abbrechens mit einem CancellationToken

    CancellationToken token = ...;
    await SomeMethodAsync().AsTask(token);

  • Unterstützen von Fortschrittsberichten mit IProgress<T>
    IProgress<TProgress> progress = ...;
    await SomeMethodAsync().AsTask(progress);
  • Unterdrücken des Marshalling-Standardverhaltens für die Fortsetzung mit ConfigureAwait
    await SomeMethodAsync().AsTask().ConfigureAwait(false);

AsTask ist auch in weiteren Situationen nützlich.

Eine davon steht mit der Möglichkeit im Zusammenhang, mit Task mehrere Fortsetzungsvorgänge zu unterstützen. Die asynchronen WinRT-Vorgänge unterstützen ausschließlich einen einzelnen, über Completed registrierten Delegaten, der nur einmal festgelegt werden kann. (Bei Completed handelt es sich eher um eine Eigenschaft als um ein Ereignis.) Dies ist für die meisten Fälle geeignet, in denen einfach einmal ein Vorgang erwartet werden soll, beispielsweise anstelle des Aufrufens einer synchronen Methode:

SomeMethod();

Sie rufen ein asynchrones Gegenstück auf, und erwarten dieses:

await SomeMethodAsync();

Hierbei wird dieselbe Ablaufsteuerung beibehalten wie bei Verwendung des synchronen Gegenstücks. In einigen Fällen müssen jedoch mehrere Callbacks verknüpft oder dieselbe Instanz mehrfach erwartet werden können. Im Gegensatz zu den asynchronen WinRT-Schnittstellen unterstützt der Task-Typ eine beliebige Anzahl von „await“-Vorgängen oder eine beliebige Anzahl an Verwendungen von dessen ContinueWith-Methode. Dies ermöglicht mehrfache Callbacks. Daher können Sie AsTask verwenden, um einen Task für den asynchronen WinRT-Vorgang abzurufen und anschließend mehrere Callbacks mit dem Task anstatt direkt mit dem asynchronen WinRT-Vorgang zu verknüpfen.

var t = SomeMethodAsync().AsTask();
t.ContinueWith(delegate { ... });
t.ContinueWith(delegate { ... });
t.ContinueWith(delegate { ... });

Ein weiteres Beispiel für die Nützlichkeit von AsTask ist der Umgang mit Methoden, die in Bezug auf die Typen Task oder Task<TResult> ausgeführt werden. Kombinatormethoden wie Task.WhenAll oder Task.WhenAny werden in Bezug auf Task ausgeführt und nicht in Bezug auf die asynchronen WinRT-Schnittstellen. Wenn Sie also mehrere asynchrone WinRT-Vorgänge aufrufen und anschließend mit await auf den Abschluss von allen oder einem dieser Vorgänge warten möchten, können Sie dies mit AsTask einfach umsetzen. Beispielsweise wird dieser await abgeschlossen, sobald einer der drei bereitgestellten Vorgänge abgeschlossen ist. Der entsprechende Task wird zurückgegeben:

Task firstCompleted = await Task.WhenAny(
SomeMethod1Async().AsTask(),
SomeMethod2Async().AsTask(),
SomeMethod3Async().AsTask());

Fazit

Es ist wirklich spannend, wie viele WinRT-Funktionen mithilfe von asynchronen Vorgängen bereitgestellt werden. Der Umfang der entsprechenden vorgestellten APIs spricht für den hohen Stellenwert, den die Reaktionsfähigkeit der Plattform einnimmt. Diese wiederum stellt bedeutende Anforderungen an das Programmiermodell, mit dem diese Vorgänge umgesetzt werden: Für Code in C# und Visual Basic bieten await und AsTask die entsprechende Antwort. Ich hoffe, dass Ihnen dieser Beitrag genügend Informationen über die Hintergründe geliefert hat, um ein umfassendes Verständnis der Funktionsweise zu bieten und die produktive Entwicklung von Apps im Metro-Stil zu ermöglichen.

Weitere Informationen finden Sie in diesen Ressourcen:

Stephen Toub
Visual Studio