Freigeben über


Verwenden des aufgabenbasierten asynchronen Musters

Wenn Sie das aufgabenbasierte asynchrone Muster (TAP) verwenden, um mit asynchronen Vorgängen zu arbeiten, können Sie Rückrufe verwenden, um ein Warten ohne Blockierung zu erreichen. Für Aufgaben wird dies durch Methoden wie z. B. Task.ContinueWith erreicht. Sprachbasierte asynchrone Unterstützung blendet Rückrufe aus, indem asynchrone Vorgänge im normalen Steuerungsfluss erwartet werden können, und vom Compiler generierter Code bietet die gleiche UNTERSTÜTZUNG auf API-Ebene.

Anhalten der Ausführung mit „await“

Sie können das await-Schlüsselwort in C# und den Await-Operator in Visual Basic verwenden, um asynchron auf Task und Task<TResult> Objekte zu warten. Wenn Sie eine Task-Klasse erwarten, ist der Ausdruck await vom Typ void. Wenn Sie eine Task<TResult>-Klasse erwarten, ist der Ausdruck await vom Typ TResult. Ein await-Ausdruck muss im Text einer asynchronen Methode auftreten. (Diese Sprachfeatures wurden in .NET Framework 4.5 eingeführt.)

Die await-Funktionalität installiert im Hintergrund einen Rückruf für die Aufgabe, indem sie eine Fortsetzung verwendet. Dieser Rückruf setzt die asynchrone Methode an dem Unterbrechungspunkt fort. Wenn die asynchrone Methode fortgesetzt wird und der Vorgang, auf den gewartet wurde, erfolgreich abgeschlossen wurde und Task<TResult> war, wird TResult zurückgegeben. Wenn die erwartete Klasse Task oder Task<TResult> im Zustand Canceled beendet wurde, wird eine OperationCanceledException-Ausnahme ausgelöst. Wenn die erwartete Klasse Task oder Task<TResult> im Zustand Faulted beendet wurde, wird die für dessen fehlerhafte Ausführung verantwortliche Ausnahme ausgelöst. Ein Task-Objekt kann infolge mehrerer Ausnahmen einen Fehler verursachen, aber nur eine dieser Ausnahmen wird weitergegeben. Die Task.Exception Eigenschaft gibt jedoch eine AggregateException Ausnahme zurück, die alle Fehler enthält.

Wenn ein Synchronisierungskontext (SynchronizationContext-Objekt) mit dem Thread verknüpft wird, der zum Zeitpunkt der Unterbrechung die asynchrone Methode ausgeführt hat (z. B. wenn die Eigenschaft SynchronizationContext.Current nicht null ist), wird die asynchrone Methode für diesen Synchronisierungskontext mit der Post-Methode des Kontexts fortgesetzt. Andernfalls basiert sie auf dem Vorgangsplaner (TaskScheduler Objekt), der zum Zeitpunkt der Aussetzung aktuell war. In der Regel ist dies der Standardaufgabenplaner (TaskScheduler.Default), der auf den Threadpool ausgerichtet ist. Dieser Vorgangsplaner bestimmt, ob der erwartete asynchrone Vorgang fortgesetzt werden soll, wo er abgeschlossen wurde oder ob die Wiederaufnahme geplant werden soll. Der Standardzeitplaner ermöglicht in der Regel, dass die Fortsetzung auf dem Thread ausgeführt wird, den der erwartete Vorgang abgeschlossen hat.

Wenn eine asynchrone Methode aufgerufen wird, führt diese synchron den Hauptteil der Funktion bis zum ersten await-Ausdruck in einer await-fähigen Instanz aus, die noch nicht abgeschlossen ist. Dies ist der Punkt, an dem der Aufruf zum Aufrufer zurückkehrt. Wenn die asynchrone Methode nicht void zurückgibt, wird ein Task oder Task<TResult> Objekt zurückgegeben, um die fortlaufende Berechnung darzustellen. Wird in einer asynchronen Methode, die nicht „void“ zurückgibt, eine return-Anweisung gefunden oder das Ende des Methodentexts erreicht, wird der Task im RanToCompletion-Endzustand abgeschlossen. Wenn ein Ausnahmefehler bewirkt, dass die Steuerung den Text der asynchronen Methode verlässt, endet der Task im Faulted-Zustand. Wenn es sich bei dieser Ausnahme um eine OperationCanceledExceptionAusnahme handelt, endet die Aufgabe stattdessen im Canceled Zustand. Auf diese Weise wird das Ergebnis oder die Ausnahme schließlich veröffentlicht.

Es gibt mehrere wichtige Variationen dieses Verhaltens. Aus Leistungsgründen wird, wenn eine Aufgabe zu dem Zeitpunkt, zu dem sie erwartet wurde, bereits abgeschlossen ist, die Steuerung nicht abgegeben, sondern die Ausführung der Funktion fortgesetzt. Darüber hinaus ist die Rückkehr zum ursprünglichen Kontext nicht immer das gewünschte Verhalten und kann geändert werden. dies wird im nächsten Abschnitt ausführlicher beschrieben.

Konfigurieren von Unterbrechung und Wiederaufnahme mit „Yield“ und „ConfigureAwait“

Mehrere Methoden bieten mehr Kontrolle über die Ausführung einer asynchronen Methode. Sie können z. B. die Task.Yield Methode verwenden, um einen Ertragspunkt in die asynchrone Methode einzufügen:

public class Task : …
{
    public static YieldAwaitable Yield();
    …
}

Dies entspricht einem asynchronen Senden oder Planen zurück zum aktuellen Kontext.

Task.Run(async delegate
{
    for(int i=0; i<1000000; i++)
    {
        await Task.Yield(); // fork the continuation into a separate work item
        ...
    }
});

Sie können auch die Task.ConfigureAwait Methode verwenden, um eine bessere Steuerung des Anhaltens und der Wiederaufnahme in einer asynchronen Methode zu ermöglichen. Wie bereits erwähnt, wird der aktuelle Kontext standardmäßig beim Anhalten einer asynchronen Methode erfasst, und dieser erfasste Kontext wird verwendet, um die Fortsetzung der asynchronen Methode bei der Wiederaufnahme aufzurufen. In vielen Fällen ist dies das genaue Verhalten, das Sie benötigen. In anderen Fällen interessiert Sie der Fortsetzungskontext möglicherweise nicht, und Sie können eine bessere Leistung erzielen, indem Sie es vermeiden, solche Rückführungen in den ursprünglichen Kontext vorzunehmen. Um dies zu aktivieren, verwenden Sie die Task.ConfigureAwait Methode, um dem Await-Vorgang mitzuteilen, den Kontext nicht zu erfassen und fortzusetzen, sondern die Ausführung dort fortzusetzen, wo der asynchrone Vorgang, der gewartet wurde, abgeschlossen wurde.

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Abbrechen eines asynchronen Vorgangs

Ab .NET Framework 4 bieten TAP-Methoden, die den Abbruch unterstützen, mindestens eine Überladung, die ein Abbruchtoken (CancellationToken Objekt) akzeptiert.

Ein Abbruchtoken wird über eine Abbruchtokenquelle (CancellationTokenSource Objekt) erstellt. Die Token-Eigenschaft der Quelle gibt das Abbruchtoken zurück, das signalisiert wird, wenn die Methode Cancel der Quelle aufgerufen wird. Wenn Sie z. B. eine einzelne Webseite herunterladen möchten und den Vorgang abbrechen möchten, erstellen Sie ein CancellationTokenSource Objekt, übergeben das Token an die TAP-Methode und rufen dann die Methode der Quelle Cancel auf, wenn Sie bereit sind, den Vorgang abzubrechen:

var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();

Um mehrere asynchrone Aufrufe abzubrechen, können Sie dasselbe Token an alle Aufrufe übergeben:

var cts = new CancellationTokenSource();
    IList<string> results = await Task.WhenAll(from url in urls select DownloadStringTaskAsync(url, cts.Token));
    // at some point later, potentially on another thread
    …
    cts.Cancel();

Oder Sie können dasselbe Token an eine selektive Teilmenge von Vorgängen übergeben:

var cts = new CancellationTokenSource();
    byte [] data = await DownloadDataAsync(url, cts.Token);
    await SaveToDiskAsync(outputPath, data, CancellationToken.None);
    … // at some point later, potentially on another thread
    cts.Cancel();

Von Bedeutung

Abbruchanforderungen können von jedem Thread initiiert werden.

Sie können den CancellationToken.None Wert an jede Methode übergeben, die ein Abbruchtoken akzeptiert, um anzugeben, dass der Abbruch nie angefordert wird. Dies bewirkt, dass die CancellationToken.CanBeCanceled Eigenschaft zurückgegeben falsewird, und die aufgerufene Methode kann entsprechend optimiert werden. Zu Testzwecken können Sie auch ein vorab abgebrochenes Abbruchtoken übergeben, das mit dem Konstruktor instanziiert wurde, der einen booleschen Wert akzeptiert, um anzugeben, ob das Token in einem bereits abgebrochenen oder in einem nicht abbrechbaren Zustand beginnen soll.

Dieser Ansatz zur Stornierung hat mehrere Vorteile:

  • Sie können dasselbe Abbruchtoken an eine beliebige Anzahl asynchroner und synchroner Vorgänge übergeben.

  • Die gleiche Abbruchanforderung kann an eine beliebige Anzahl von Listenern proliferiert werden.

  • Der Entwickler der asynchronen API ist in der vollständigen Kontrolle darüber, ob der Abbruch angefordert werden kann und wann er wirksam wird.

  • Der Code, der die API verwendet, bestimmt möglicherweise selektiv die asynchronen Aufrufe, an die Abbruchanforderungen weitergegeben werden.

Überwachen des Fortschritts

Einige asynchrone Methoden machen den Fortschritt über eine Fortschrittsschnittstelle verfügbar, die an die asynchrone Methode übergeben wird. Betrachten Sie beispielsweise eine Funktion, die asynchron eine Textzeichenfolge herunterlädt, und löst auf dem Weg Fortschrittsaktualisierungen aus, die den Prozentsatz des Downloads enthalten, der bisher abgeschlossen wurde. Eine solche Methode kann in einer Windows Presentation Foundation (WPF)-Anwendung wie folgt verwendet werden:

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
            new Progress<int>(p => pbDownloadProgress.Value = p));
    }
    finally { btnDownload.IsEnabled = true; }
}

Verwenden der integrierten aufgabenbasierten Kombinatoren

Der System.Threading.Tasks Namespace enthält mehrere Methoden zum Verfassen und Arbeiten mit Aufgaben.

Task.Run

Die Task Klasse enthält mehrere Run Methoden, mit denen Sie arbeit einfach als Task oder Task<TResult> in den Threadpool entladen können, z. B.:

public async void button1_Click(object sender, EventArgs e)
{
    textBox1.Text = await Task.Run(() =>
    {
        // … do compute-bound work here
        return answer;
    });
}

Einige dieser Run-Methoden wie die Task.Run(Func<Task>)-Überladung sind als Kurzform für die TaskFactory.StartNew-Methode verfügbar. Mit dieser Überladung können Sie 'await' innerhalb der ausgelagerten Aufgaben verwenden, zum Beispiel:

public async void button1_Click(object sender, EventArgs e)
{
    pictureBox1.Image = await Task.Run(async() =>
    {
        using(Bitmap bmp1 = await DownloadFirstImageAsync())
        using(Bitmap bmp2 = await DownloadSecondImageAsync())
        return Mashup(bmp1, bmp2);
    });
}

Solche Überladungen entsprechen logisch der Verwendung der TaskFactory.StartNew Methode in Verbindung mit der Unwrap Erweiterungsmethode in der Task Parallel Library.

Task.FromResult

Verwenden Sie die FromResult-Methode für Szenarien, in denen Daten möglicherweise bereits verfügbar sind und lediglich von einer Methode zurückgegeben werden müssen, die eine Aufgabe zurückgibt und sie in ein Task<TResult>-Objekt übergibt:

public Task<int> GetValueAsync(string key)
{
    int cachedValue;
    return TryGetCachedValue(out cachedValue) ?
        Task.FromResult(cachedValue) :
        GetValueAsyncInternal();
}

private async Task<int> GetValueAsyncInternal(string key)
{
    …
}

Task.WhenAll

Verwenden Sie die WhenAll Methode, um asynchron auf mehrere asynchrone Vorgänge zu warten, die als Aufgaben dargestellt werden. Die Methode verfügt über mehrere Überladungen, die eine Reihe nicht generischer Aufgaben oder eine nicht einheitliche Gruppe von generischen Aufgaben unterstützen (z. B. asynchrones Warten auf mehrere Operationen, die keinen Wert zurückgeben, oder asynchrones Warten auf mehrere Methoden, die Werte zurückgeben, wobei jeder Wert möglicherweise einen anderen Typ hat) sowie einen einheitlichen Satz generischer Aufgaben unterstützen (z. B. asynchrones Warten auf mehrere TResult-Rückgabemethoden).

Angenommen, Sie möchten E-Mail-Nachrichten an mehrere Kunden senden. Sie können das Senden der Nachrichten überlappen, sodass Sie nicht darauf warten, dass eine Nachricht abgeschlossen ist, bevor Sie die nächste Nachricht senden. Sie können auch herausfinden, wann die Sendevorgänge abgeschlossen wurden und ob Fehler aufgetreten sind:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);

Dieser Code behandelt auftretende Ausnahmen nicht explizit, sondern ermöglicht es, Ausnahmen aus dem await-Vorgang für die resultierende Aufgabe aus WhenAll weiterzugeben. Um die Ausnahmen zu behandeln, können Sie Code wie die folgenden verwenden:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    ...
}

Wenn ein asynchroner Vorgang fehlschlägt, werden alle Ausnahmen in einer AggregateException Ausnahme konsolidiert, die in der Task von der WhenAll Methode zurückgegebenen gespeichert wird. Allerdings wird nur eine dieser Ausnahmen vom await Schlüsselwort weitergegeben. Wenn Sie alle Ausnahmen untersuchen möchten, können Sie den vorherigen Code wie folgt umschreiben:

Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Betrachten wir ein Beispiel für das asynchrone Herunterladen mehrerer Dateien aus dem Web. In diesem Fall verfügen alle asynchronen Vorgänge über homogene Ergebnistypen, und es ist einfach, auf die Ergebnisse zuzugreifen:

string [] pages = await Task.WhenAll(
    from url in urls select DownloadStringTaskAsync(url));

Sie können die gleichen Verarbeitungstechniken für Ausnahmen verwenden, die im vorherigen Szenario mit „void“-Rückgabe erläutert wurden:

Task<string> [] asyncOps =
    (from url in urls select DownloadStringTaskAsync(url)).ToArray();
try
{
    string [] pages = await Task.WhenAll(asyncOps);
    ...
}
catch(Exception exc)
{
    foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Task.WhenAny

Sie können die WhenAny Methode verwenden, um asynchron auf einen von mehreren asynchronen Vorgängen zu warten, die als Aufgaben ausgeführt werden. Diese Methode dient vier primären Anwendungsfällen:

  • Redundanz: Führen Sie einen Vorgang mehrmals aus, und wählen Sie das zuerst abgeschlossene aus (z. B. das Kontaktieren mehrerer Aktienkurse-Webdienste, die ein einzelnes Ergebnis erzeugen und das Ergebnis auswählen, das am schnellsten abgeschlossen ist).

  • Überlappung: Starten von mehreren Vorgängen und Warten, bis alle abgeschlossen sind, aber Verarbeiten der Vorgänge, sobald sie abgeschlossen sind.

  • Einschränkung: Zulassen, dass weitere Vorgänge gestartet werden, während andere abgeschlossen werden. Dies ist eine Erweiterung des Szenarios mit Überlappung.

  • Vorzeitiger Hashabbruch: Ein von Task t1 dargestellter Vorgang kann in einem WhenAny-Task mit einem anderen Task t2 gruppiert werden, und Sie können auf den Task WhenAny warten. Aufgabe t2 kann ein Timeout oder einen Abbruch oder ein anderes Signal darstellen, das dazu führt, dass die WhenAny Aufgabe abgeschlossen wird, bevor t1 abgeschlossen wird.

Redundanz

Berücksichtigen Sie einen Fall, in dem Sie eine Entscheidung darüber treffen möchten, ob Sie eine Aktie kaufen möchten. Es gibt mehrere Webdienste für Aktienempfehlungen, denen Sie vertrauen, aber abhängig von der täglichen Auslastung kann jeder Dienst zu unterschiedlichen Zeiten langsam sein. Sie können die WhenAny Methode verwenden, um eine Benachrichtigung zu erhalten, wenn ein Vorgang abgeschlossen ist:

var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol),
    GetBuyRecommendation2Async(symbol),
    GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);

Im Gegensatz zur WhenAll-Methode, mit der die entpackten Ergebnisse aller erfolgreich abgeschlossenen Tasks zurückgegeben werden, gibt WhenAny den abgeschlossenen Task zurück. Wenn ein Vorgang fehlschlägt, ist es wichtig zu wissen, dass er fehlgeschlagen ist, und wenn eine Aufgabe erfolgreich ist, ist es wichtig zu wissen, welcher Vorgang der Rückgabewert zugeordnet ist. Daher müssen Sie auf das Ergebnis der zurückgegebenen Aufgabe zugreifen oder es weiter erwarten, wie in diesem Beispiel gezeigt.

Wie bei WhenAll müssen Sie Ausnahmen berücksichtigen können. Da Sie die abgeschlossene Aufgabe zurückerhalten, können Sie die zurückgegebene Aufgabe über „await“ veranlassen, Fehler weiterzugeben, und try/catch verwenden, um die Fehler entsprechend zu behandeln. Beispiel:

Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
    Task<bool> recommendation = await Task.WhenAny(recommendations);
    try
    {
        if (await recommendation) BuyStock(symbol);
        break;
    }
    catch(WebException exc)
    {
        recommendations.Remove(recommendation);
    }
}

Auch wenn ein erster Vorgang erfolgreich abgeschlossen wird, können nachfolgende Vorgänge fehlschlagen. An diesem Punkt haben Sie mehrere Optionen für den Umgang mit Ausnahmen: Sie können warten, bis alle gestarteten Aufgaben abgeschlossen sind, in diesem Fall können Sie die WhenAll Methode verwenden, oder Sie können entscheiden, dass alle Ausnahmen wichtig sind und protokolliert werden müssen. Dazu können Sie Fortsetzungen verwenden, um eine Benachrichtigung zu erhalten, wenn Aufgaben asynchron abgeschlossen wurden:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => { if (t.IsFaulted) Log(t.Exception); });
}

oder:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}

oder sogar:

private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
    foreach(var task in tasks)
    {
        try { await task; }
        catch(Exception exc) { Log(exc); }
    }
}
…
LogCompletionIfFailed(recommendations);

Schließlich möchten Sie möglicherweise alle verbleibenden Vorgänge abbrechen:

var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol, cts.Token),
    GetBuyRecommendation2Async(symbol, cts.Token),
    GetBuyRecommendation3Async(symbol, cts.Token)
};

Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);

Überlappen

Betrachten Sie einen Fall, in dem Sie Bilder aus dem Web herunterladen und jedes Bild verarbeiten (z. B. das Hinzufügen des Bilds zu einem UI-Steuerelement). Sie verarbeiten die Bilder sequenziell im UI-Thread, möchten aber die Bilder so gleichzeitig wie möglich herunterladen. Sie möchten auch nicht das Hinzufügen der Bilder zur Benutzeroberfläche verzögern, bis sie alle heruntergeladen wurden. Sie möchten sie stattdessen hinzufügen, wenn sie fertig sind.

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

Sie können auch Interleaving auf ein Szenario anwenden, das eine rechenintensive Verarbeitung auf ThreadPool der heruntergeladenen Bilder umfasst, zum Beispiel:

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)
         .ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

Drosselung

Betrachten Sie das Interleaving-Beispiel, außer dass der Benutzer so viele Bilder herunterlädt, dass die Downloads gedrosselt werden müssen; zum Beispiel möchten Sie nur eine bestimmte Anzahl von Downloads gleichzeitig zulassen. Um dies zu erreichen, können Sie eine Teilmenge der asynchronen Vorgänge starten. Wenn Vorgänge abgeschlossen sind, können Sie zusätzliche Vorgänge starten, um deren Platz einzunehmen.

const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
    imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
    nextIndex++;
}

while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch(Exception exc) { Log(exc); }

    if (nextIndex < urls.Length)
    {
        imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
        nextIndex++;
    }
}

Vorzeitiger Abbruch

Denken Sie daran, dass Sie asynchron warten, bis ein Vorgang abgeschlossen ist, während er gleichzeitig auf die Abbruchanforderung eines Benutzers reagiert (z. B. auf eine Schaltfläche zum Abbrechen geklickt). Der folgende Code veranschaulicht dieses Szenario:

private CancellationTokenSource m_cts;

public void btnCancel_Click(object sender, EventArgs e)
{
    if (m_cts != null) m_cts.Cancel();
}

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();
    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        if (imageDownload.IsCompleted)
        {
            Bitmap image = await imageDownload;
            panel.AddImage(image);
        }
        else imageDownload.ContinueWith(t => Log(t));
    }
    finally { btnRun.Enabled = true; }
}

private static async Task UntilCompletionOrCancellation(
    Task asyncOp, CancellationToken ct)
{
    var tcs = new TaskCompletionSource<bool>();
    using(ct.Register(() => tcs.TrySetResult(true)))
        await Task.WhenAny(asyncOp, tcs.Task);
    return asyncOp;
}

Mit dieser Implementierung wird die Benutzeroberfläche erneut aktiviert, sobald die Entscheidung für den Abbruch erfolgt ist, aber die zugrundeliegenden asynchronen Vorgänge werden nicht abgebrochen. Eine weitere Alternative ist das Abbrechen der ausstehenden Vorgänge, wenn die Entscheidung für den Abbruch erfolgt ist, aber mit dem erneuten Aktivieren der Benutzeroberfläche zu warten, bis die Vorgänge abgeschlossen sind, möglicherweise weil sie wegen der Abbruchanforderung vorzeitig zu beenden sind:

private CancellationTokenSource m_cts;

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();

    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        Bitmap image = await imageDownload;
        panel.AddImage(image);
    }
    catch(OperationCanceledException) {}
    finally { btnRun.Enabled = true; }
}

Ein weiteres Beispiel für eine frühzeitige Rettungsaktion umfasst die Verwendung der WhenAny Methode in Verbindung mit der Delay Methode, wie im nächsten Abschnitt erläutert.

Task.Delay

Mit der Task.Delay Methode können Sie Pausen in die Ausführung einer asynchronen Methode einführen. Dies ist nützlich für viele Arten von Funktionen, z. B. das Erstellen von Abrufschleifen und das Verzögern der Behandlung von Benutzereingaben für einen vordefinierten Zeitraum. Die Task.Delay-Methode kann auch in Kombination mit Task.WhenAny nützlich sein, um Timeouts bei Awaits zu implementieren.

Wenn eine Aufgabe, die Teil eines größeren asynchronen Vorgangs ist (z. B. ein ASP.NET Webdienst), zu lange dauert, kann der Gesamtvorgang leiden, insbesondere, wenn er nie abgeschlossen werden kann. Aus diesem Grund ist es wichtig, ein Timeout festlegen zu können, wenn Sie auf einen asynchronen Vorgang warten. Die synchronen Task.Wait, Task.WaitAll, und Task.WaitAny Methoden akzeptieren Timeout-Werte, aber die entsprechenden TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny und die zuvor erwähnten Task.WhenAll/Task.WhenAny Methoden nicht. Stattdessen können Sie Task.Delay und Task.WhenAny in Kombination verwenden, um ein Timeout zu implementieren.

Nehmen wir beispielsweise in Ihrer UI-Anwendung an, dass Sie ein Bild herunterladen und die Benutzeroberfläche deaktivieren möchten, während das Bild heruntergeladen wird. Wenn der Download jedoch zu lange dauert, möchten Sie die Benutzeroberfläche erneut aktivieren und den Download verwerfen:

public async void btnDownload_Click(object sender, EventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap> download = GetBitmapAsync(url);
        if (download == await Task.WhenAny(download, Task.Delay(3000)))
        {
            Bitmap bmp = await download;
            pictureBox.Image = bmp;
            status.Text = "Downloaded";
        }
        else
        {
            pictureBox.Image = null;
            status.Text = "Timed out";
            var ignored = download.ContinueWith(
                t => Trace("Task finally completed"));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Dasselbe gilt für mehrere Downloads, weil WhenAll eine Aufgabe zurückgibt.

public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap[]> downloads =
            Task.WhenAll(from url in urls select GetBitmapAsync(url));
        if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
        {
            foreach(var bmp in downloads.Result) panel.AddImage(bmp);
            status.Text = "Downloaded";
        }
        else
        {
            status.Text = "Timed out";
            downloads.ContinueWith(t => Log(t));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Erstellen von aufgabenbasierten Kombinatoren

Da eine Aufgabe einen asynchronen Vorgang vollständig darstellen kann und synchrone und asynchrone Funktionen für die Verknüpfung mit dem Vorgang, das Abrufen der Ergebnisse usw. bereitstellen kann, können Sie nützliche Bibliotheken von Kombinationsmodulen erstellen, die Aufgaben erstellen, um größere Muster zu erstellen. Wie im vorherigen Abschnitt erläutert, enthält .NET mehrere integrierte Kombinatoren, Aber Sie können auch eigene erstellen. Die folgenden Abschnitte enthalten mehrere Beispiele für mögliche Kombinationsmethoden und -typen.

RetryOnFault

In vielen Situationen sollten Sie einen Vorgang wiederholen, wenn ein vorheriger Versuch fehlschlägt. Für synchronen Code können Sie eine Hilfsmethode wie im folgenden Beispiel erstellen, um dies zu erreichen: RetryOnFault.

public static T RetryOnFault<T>(
    Func<T> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return function(); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

Sie können eine nahezu identische Hilfsmethode für asynchrone Vorgänge erstellen, die mit TAP implementiert werden und somit Aufgaben zurückgeben:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

Anschließend können Sie diesen Kombinationsator verwenden, um Wiederholungen in die Logik der Anwendung zu codieren. Zum Beispiel:

// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3);

Sie können die RetryOnFault Funktion weiter erweitern. Beispielsweise kann die Funktion eine andere Func<Task> annehmen, die zwischen Wiederholungen aufgerufen wird, um zu bestimmen, wann der Vorgang erneut versucht werden soll. Beispiel:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
        await retryWhen().ConfigureAwait(false);
    }
    return default(T);
}

Sie können dann die Funktion wie folgt verwenden, um eine Sekunde zu warten, bevor Sie den Vorgang wiederholen:

// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3, () => Task.Delay(1000));

NeedOnlyOne

Manchmal können Sie redundanzen nutzen, um die Latenz und Erfolgsaussichten eines Vorgangs zu verbessern. Erwägen Sie mehrere Webdienste, die Aktienkurse bereitstellen, aber zu verschiedenen Tageszeiten können jeder Dienst unterschiedliche Qualitäts- und Reaktionszeiten bereitstellen. Um diese Schwankungen zu bewältigen, können Sie Anforderungen an alle Webdienste ausgeben, und sobald Sie eine Antwort von einem erhalten, brechen Sie die verbleibenden Anforderungen ab. Sie können eine Hilfsfunktion implementieren, um die Implementierung dieses gängigen Musters für das Starten mehrerer Vorgänge zu vereinfachen, auf alle Vorgänge zu warten und dann den Rest abzubrechen. Die NeedOnlyOne Funktion im folgenden Beispiel veranschaulicht dieses Szenario:

public static async Task<T> NeedOnlyOne(
    params Func<CancellationToken,Task<T>> [] functions)
{
    var cts = new CancellationTokenSource();
    var tasks = (from function in functions
                 select function(cts.Token)).ToArray();
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach(var task in tasks)
    {
        var ignored = task.ContinueWith(
            t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return completed;
}

Sie können diese Funktion dann wie folgt verwenden:

double currentPrice = await NeedOnlyOne(
    ct => GetCurrentPriceFromServer1Async("msft", ct),
    ct => GetCurrentPriceFromServer2Async("msft", ct),
    ct => GetCurrentPriceFromServer3Async("msft", ct));

Überlappende Vorgänge

Es gibt ein potenzielles Leistungsproblem bei der Verwendung der WhenAny Methode zur Unterstützung eines verschachtelten Szenarios, insbesondere wenn Sie mit großen Aufgabenmengen arbeiten. Jeder Aufruf zu WhenAny führt dazu, dass eine Fortsetzung bei jeder Aufgabe registriert wird. Für N als Anzahl von Aufgaben führt dieses zu O(N2) Fortsetzungen, die über die Lebensdauer des überlappenden Vorgangs erstellt werden. Wenn Sie mit einer großen Gruppe von Aufgaben arbeiten, können Sie einen Kombinator (Interleaved im folgenden Beispiel) verwenden, um das Leistungsproblem zu beheben:

static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
    var inputTasks = tasks.ToList();
    var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
                   select new TaskCompletionSource<T>()).ToList();
    int nextTaskIndex = -1;
    foreach (var inputTask in inputTasks)
    {
        inputTask.ContinueWith(completed =>
        {
            var source = sources[Interlocked.Increment(ref nextTaskIndex)];
            if (completed.IsFaulted)
                source.TrySetException(completed.Exception.InnerExceptions);
            else if (completed.IsCanceled)
                source.TrySetCanceled();
            else
                source.TrySetResult(completed.Result);
        }, CancellationToken.None,
           TaskContinuationOptions.ExecuteSynchronously,
           TaskScheduler.Default);
    }
    return from source in sources
           select source.Task;
}

Anschließend können Sie den Kombinator verwenden, um die Ergebnisse von Vorgängen während der Ausführung zu verarbeiten. Zum Beispiel:

IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
    int result = await task;
    …
}

WhenAllOrFirstException

In bestimmten Scatter-Gather-Szenarien möchten Sie möglicherweise auf alle Aufgaben in einem Satz warten, es sei denn, bei einer dieser Aufgaben tritt ein Fehler auf. In diesem Fall soll das Warten beendet werden, sobald die Ausnahme auftritt. Sie können dies mit einer Kombinationsmethode wie WhenAllOrFirstException im folgenden Beispiel erreichen:

public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
    var inputs = tasks.ToList();
    var ce = new CountdownEvent(inputs.Count);
    var tcs = new TaskCompletionSource<T[]>();

    Action<Task> onCompleted = (Task completed) =>
    {
        if (completed.IsFaulted)
            tcs.TrySetException(completed.Exception.InnerExceptions);
        if (ce.Signal() && !tcs.Task.IsCompleted)
            tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
    };

    foreach (var t in inputs) t.ContinueWith(onCompleted);
    return tcs.Task;
}

Erstellen aufgabenbasierter Datenstrukturen

Neben der Möglichkeit, benutzerdefinierte aufgabenbasierte Kombinatoren zu erstellen, gibt es eine Datenstruktur in Task und Task<TResult>, die sowohl die Ergebnisse eines asynchronen Vorgangs als auch die notwendigen Schritte zur Synchronisierung mit dieser darstellt, wodurch sie zu einem leistungsfähigen Typ wird, auf dem benutzerdefinierte Datenstrukturen erstellt werden können, die in asynchronen Szenarien verwendet werden sollen.

AsyncCache

Ein wichtiger Aspekt eines Tasks besteht darin, dass dieser an mehrere Consumer ausgegeben werden kann, die u.a. alle auf diesen warten, Fortsetzungen bei diesem registrieren und dessen Ergebnis oder dessen Ausnahmen abrufen können (im Fall von Task<TResult>). Dies macht Task und Task<TResult> perfekt geeignet für die Verwendung in einer asynchronen Caching-Infrastruktur. Hier ist ein Beispiel für einen kleinen, aber leistungsstarken asynchronen Cache, der auf folgendem Task<TResult>Beispiel basiert:

public class AsyncCache<TKey, TValue>
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

    public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
    {
        if (valueFactory == null) throw new ArgumentNullException("valueFactory");
        _valueFactory = valueFactory;
        _map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
    }

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException("key");
            return _map.GetOrAdd(key, toAdd =>
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}

Die AsyncCache<TKey,TValue-Klasse> akzeptiert als Delegat für seinen Konstruktor eine Funktion, die ein TKey nimmt und ein Task<TResult> zurückgibt. Alle zuvor aus dem Cache abgerufenen Werte werden im internen Wörterbuch gespeichert, und AsyncCache stellt sicher, dass nur eine Aufgabe pro Schlüssel generiert wird, selbst wenn gleichzeitig auf den Cache zugegriffen wird.

Sie können beispielsweise einen Cache für heruntergeladene Webseiten erstellen:

private AsyncCache<string,string> m_webPages =
    new AsyncCache<string,string>(DownloadStringTaskAsync);

Sie können diesen Cache dann immer dann in asynchronen Methoden verwenden, wenn Sie den Inhalt einer Webseite benötigen. Die AsyncCache Klasse stellt sicher, dass Sie so wenige Seiten wie möglich herunterladen und die Ergebnisse zwischenspeichern.

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtContents.Text = await m_webPages["https://www.microsoft.com"];
    }
    finally { btnDownload.IsEnabled = true; }
}

AsyncProducerConsumerCollection

Sie können auch Aufgaben verwenden, um Datenstrukturen für die Koordination asynchroner Aktivitäten zu erstellen. Betrachten Sie eines der klassischen parallelen Designmuster: Produzent/Verbraucher. In diesem Muster generieren Produzenten Daten, die von Konsumenten genutzt werden, und die Produzenten und Konsumenten können parallel laufen. Beispielsweise verarbeitet der Verbraucher Artikel 1, der zuvor von einem Hersteller generiert wurde, der jetzt Artikel 2 produziert. Für das Erzeuger-/Verbrauchermuster benötigen Sie unveränderlich eine Datenstruktur, um die von den Herstellern erstellten Arbeiten zu speichern, damit die Verbraucher möglicherweise über neue Daten benachrichtigt werden und sie bei Bedarf finden.

Hier ist eine einfache Datenstruktur, die auf Aufgaben aufgebaut ist und die es ermöglicht, asynchrone Methoden als Erzeuger und Verbraucher zu verwenden:

public class AsyncProducerConsumerCollection<T>
{
    private readonly Queue<T> m_collection = new Queue<T>();
    private readonly Queue<TaskCompletionSource<T>> m_waiting =
        new Queue<TaskCompletionSource<T>>();

    public void Add(T item)
    {
        TaskCompletionSource<T> tcs = null;
        lock (m_collection)
        {
            if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
            else m_collection.Enqueue(item);
        }
        if (tcs != null) tcs.TrySetResult(item);
    }

    public Task<T> Take()
    {
        lock (m_collection)
        {
            if (m_collection.Count > 0)
            {
                return Task.FromResult(m_collection.Dequeue());
            }
            else
            {
                var tcs = new TaskCompletionSource<T>();
                m_waiting.Enqueue(tcs);
                return tcs.Task;
            }
        }
    }
}

Mit dieser Datenstruktur können Sie Code wie die folgenden schreiben:

private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.Take();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Add(data);
}

Der System.Threading.Tasks.Dataflow-Namespace enthält den BufferBlock<T>-Typ, den Sie ähnlich verwenden können, jedoch ohne einen benutzerdefinierten Sammlungstyp erstellen zu müssen.

private static BufferBlock<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.ReceiveAsync();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Post(data);
}

Hinweis

Der System.Threading.Tasks.Dataflow Namespace ist als NuGet-Paket verfügbar. Um die Assembly zu installieren, die den System.Threading.Tasks.Dataflow Namespace enthält, öffnen Sie Ihr Projekt in Visual Studio, wählen Sie " NuGet-Pakete verwalten" im Menü "Projekt" aus, und suchen Sie online nach dem System.Threading.Tasks.Dataflow Paket.

Siehe auch