Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
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 verwendet dieses Muster Methoden wie Task.ContinueWith. 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 auf einen TaskAusdruck warten, ist der await Ausdruck vom Typ void. Wenn Sie auf einen Task<TResult>Ausdruck warten, ist der await Ausdruck 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 Sie eine asynchrone Methode aufrufen, wird der Rumpf der Funktion synchron ausgeführt, bis auf einer noch nicht abgeschlossenen Instanz der erste Await-Ausdruck erreicht wird, woraufhin die Ausführung zum Aufrufer zurückkehrt. Wenn die asynchrone Methode kein void zurückgibt, gibt sie ein Task- oder Task<TResult>-Objekt zurück, das die laufende Berechnung darstellt. 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: Wenn eine Aufgabe bereits abgeschlossen ist, bevor sie erwartet wird, wird die Kontrolle nicht abgegeben, und die Funktion wird weiterhin ausgeführt. Darüber hinaus ist die Rückkehr zum ursprünglichen Kontext nicht immer das gewünschte Verhalten und kann geändert werden. Dieses Verhalten 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();
…
}
Diese Methode entspricht dem asynchronen Posten oder Planen, um in den aktuellen Kontext zurückzukehren.
public static async Task YieldLoopExample()
{
await Task.Run(async delegate
{
for (int i = 0; i < 1000000; i++)
{
await Task.Yield(); // fork the continuation into a separate work item
}
});
}
Public Async Function YieldLoopExample() As Task
Await Task.Run(Async Function()
For i As Integer = 0 To 999999
Await Task.Yield() ' fork the continuation into a separate work item
Next
End Function)
End Function
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 interessieren Sie sich möglicherweise nicht für den Fortsetzungskontext, und Sie können eine bessere Leistung erzielen, indem Sie vermeiden, in den ursprünglichen Kontext zurückzukehren. Um dieses Verhalten zu aktivieren, verwenden Sie die Task.ConfigureAwait Methode, um den Await-Vorgang anzuweisen, den Kontext nicht zu erfassen und darauf fortzusetzen, sondern die Ausführung dort fortzusetzen, wo der asynchrone Vorgang abgeschlossen wurde.
await someTask.ConfigureAwait(continueOnCapturedContext:false);
Awaitables, ConfigureAwait und SynchronizationContext
await funktioniert mit jedem Typ, der das erwartete Ausdrucksmuster erfüllt, nicht nur Task. Ein Typ kann erwartet werden, wenn er eine kompatible GetAwaiter Methode bereitstellt, die einen Typ mit IsCompleted, OnCompletedund GetResult Member zurückgibt. In den meisten öffentlichen APIs geben Task, Task<TResult>, ValueTask oder ValueTask<TResult> zurück. Verwenden Sie benutzerdefinierte Awaitables nur für spezielle Szenarien.
Wird verwendet ConfigureAwait , wenn die Fortsetzung den Kontext des Aufrufers nicht benötigt. In App-Code, der eine Benutzeroberfläche aktualisiert, ist häufig eine Kontexterfassung erforderlich. Im wiederverwendbaren Bibliothekscode wird ConfigureAwait(false) in der Regel bevorzugt, da unnötige Kontexthüpfungen vermieden werden und das Deadlock-Risiko für blockierende Aufrufer reduziert wird.
ConfigureAwait(false) ändert die Fortsetzungsplanung und nicht ExecutionContext den Ablauf. Eine tiefergehende Erläuterung des Kontextverhaltens finden Sie unter ExecutionContext und SynchronizationContext.
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.
Sie erstellen ein Abbruchtoken über eine Abbruchtokenquelle (CancellationTokenSource Objekt). Die Token-Eigenschaft der Quelle gibt das Abbruchtoken zurück, das signalisiert, wenn die Cancel-Methode der Quelle aufgerufen wird.
var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();
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 Sie dann die Methode der Quelle Cancel auf, wenn Sie bereit sind, den Vorgang abzubrechen:
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
Jeder Thread kann Abbruchanforderungen initiieren.
Sie können den CancellationToken.None Wert an jede Methode übergeben, die ein Abbruchtoken akzeptiert, um anzugeben, dass der Abbruch nie angefordert wird. Dieser Wert 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.
Dieselbe Abbruchanforderung kann an eine beliebige Anzahl von Listenern gehen.
Der Entwickler der asynchronen API hat die vollständige Kontrolle darüber, ob abbruch angefordert werden kann und wann er wirksam wird.
Der Code, der die API verwendet, kann selektiv die asynchronen Aufrufe bestimmen, zu denen Abbruchanforderungen gehen.
Überwachen des Fortschritts
Einige asynchrone Methoden machen den Fortschritt über eine Fortschrittsschnittstelle verfügbar, die Sie an die asynchrone Methode übergeben. 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. Sie können eine solche Methode in einer Windows Presentation Foundation-Anwendung (WPF) wie folgt verwenden:
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.
Hinweis
In mehreren Codebeispielen in diesem Abschnitt wird Bitmap verwendet, die das paket System.Drawing.Common erfordert und nur für Windows unterstützt wird. Die asynchronen Muster, die sie demonstrieren, gelten auf allen Plattformen. Verwenden Sie eine plattformübergreifende Imaging-Bibliothek für nicht-Windows-Ziele.
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. Beispiel:
public static async Task TaskRunBasicExample()
{
int answer = 42;
string result = await Task.Run(() =>
{
// … do compute-bound work here
return answer.ToString();
});
Console.WriteLine(result);
}
Public Async Function TaskRunBasicExample() As Task
Dim answer As Integer = 42
Dim result As String = Await Task.Run(Function()
' … do compute-bound work here
Return answer.ToString()
End Function)
Console.WriteLine(result)
End Function
Einige dieser Run-Methoden wie die Task.Run(Func<Task>)-Überladung sind als Kurzform für die TaskFactory.StartNew-Methode verfügbar. Diese Überladung ermöglicht es Ihnen, await innerhalb der ausgelagerten Arbeit zu verwenden. Beispiel:
public static async Task TaskRunAsyncExample()
{
Bitmap image = await Task.Run(async () =>
{
using Bitmap bmp1 = await Stubs.DownloadFirstImageAsync();
using Bitmap bmp2 = await Stubs.DownloadSecondImageAsync();
return Stubs.Mashup(bmp1, bmp2);
});
}
Public Async Function TaskRunAsyncExample() As Task
Dim image As Bitmap = Await Task.Run(Async Function()
Using bmp1 As Bitmap = Await Stubs.DownloadFirstImageAsync()
Using bmp2 As Bitmap = Await Stubs.DownloadSecondImageAsync()
Return Stubs.Mashup(bmp1, bmp2)
End Using
End Using
End Function)
End Function
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 in Szenarien, in denen Daten möglicherweise bereits verfügbar sind, und Sie sie einfach von einer Aufgabe zurückgeben müssen, die in eine Task<TResult> umgewandelt wurde.
public static Task<int> GetValueAsync(string key)
{
int cachedValue;
return Stubs.TryGetCachedValue(out cachedValue) ?
Task.FromResult(cachedValue) :
GetValueAsyncInternal(key);
}
static async Task<int> GetValueAsyncInternal(string key)
{
await Task.Delay(1);
return 0;
}
Public Function GetValueAsync(key As String) As Task(Of Integer)
Dim cachedValue As Integer
If Stubs.TryGetCachedValue(cachedValue) Then
Return Task.FromResult(cachedValue)
Else
Return GetValueAsyncInternal(key)
End If
End Function
Private Async Function GetValueAsyncInternal(key As String) As Task(Of Integer)
Await Task.Delay(1)
Return 0
End Function
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. asynchron auf mehrere Vorgänge mit void-returning oder asynchron auf mehrere Rückgabemethoden mit mehreren Werten warten, bei denen jeder Wert möglicherweise einen anderen Typ aufweist) und einen einheitlichen Satz generischer Aufgaben (z. B. asynchrones Warten auf mehrere TResultRückgabemethoden) unterstützt.
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 sind und ob Fehler auftreten:
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);
Dieser Code behandelt Ausnahmen, die auftreten können, nicht explizit, lässt aber zu, dass Ausnahmen aus dem await zum resultierenden Task von WhenAll weitergegeben werden. Verwenden Sie Code wie die folgenden, um die Ausnahmen zu behandeln:
public static async Task WhenAllWithCatch()
{
IEnumerable<Task> asyncOps = from addr in Stubs.addrs select Stubs.SendMailAsync(addr);
try
{
await Task.WhenAll(asyncOps);
}
catch (Exception exc)
{
Console.WriteLine(exc);
}
}
Public Async Function WhenAllWithCatch() As Task
Dim asyncOps As IEnumerable(Of Task) = From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)
Try
Await Task.WhenAll(asyncOps)
Catch exc As Exception
Console.WriteLine(exc)
End Try
End Function
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:
public static async Task WhenAllExamineExceptions()
{
Task[] asyncOps = (from addr in Stubs.addrs select Stubs.SendMailAsync(addr)).ToArray();
try
{
await Task.WhenAll(asyncOps);
}
catch (Exception exc)
{
foreach (Task faulted in asyncOps.Where(t => t.IsFaulted))
{
Console.WriteLine($"Faulted: {faulted.Exception}");
}
}
}
Public Async Function WhenAllExamineExceptions() As Task
Dim asyncOps As Task() = (From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)).ToArray()
Try
Await Task.WhenAll(asyncOps)
Catch exc As Exception
For Each faulted As Task In asyncOps.Where(Function(t) t.IsFaulted)
Console.WriteLine($"Faulted: {faulted.Exception}")
Next
End Try
End Function
Betrachten Sie 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 Techniken für die Ausnahmebehandlung verwenden, die im vorherigen Szenario für "void-returning" erläutert werden:
public static async Task WhenAllDownloadPagesExceptions()
{
Task<string>[] asyncOps =
(from url in Stubs.urls select Stubs.DownloadStringTaskAsync(url)).ToArray();
try
{
string[] pages = await Task.WhenAll(asyncOps);
Console.WriteLine(pages.Length);
}
catch (Exception exc)
{
foreach (Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
{
Console.WriteLine($"Faulted: {faulted.Exception}");
}
}
}
Public Async Function WhenAllDownloadPagesExceptions() As Task
Dim asyncOps As Task(Of String)() =
(From url In Stubs.urls Select Stubs.DownloadStringTaskAsync(url)).ToArray()
Try
Dim pages As String() = Await Task.WhenAll(asyncOps)
Console.WriteLine(pages.Length)
Catch exc As Exception
For Each faulted As Task(Of String) In asyncOps.Where(Function(t) t.IsFaulted)
Console.WriteLine($"Faulted: {faulted.Exception}")
Next
End Try
End Function
Task.WhenAny
Verwenden Sie die WhenAny Methode, um asynchron auf nur einen von mehreren asynchronen Vorgängen zu warten, die als Aufgaben ausgeführt werden. Diese Methode dient vier primären Anwendungsfällen:
Redundanz: Mehrfaches Ausführen eines Vorgangs und Auswählen des ersten Vorgangs (z. B. Das Kontaktieren mehrerer Aktienkurse-Webdienste, die ein einzelnes Ergebnis zurückgeben 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. Dieses Szenario ist eine Erweiterung des Interleaving-Szenarios.
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 je nach täglicher Auslastung kann jeder Dienst zu unterschiedlichen Zeiten langsam sein. Verwenden Sie die WhenAny Methode, um eine Benachrichtigung zu erhalten, wenn ein Vorgang abgeschlossen ist:
public static async Task WhenAnyRedundancy(string symbol)
{
var recommendations = new List<Task<bool>>()
{
Stubs.GetBuyRecommendation1Async(symbol),
Stubs.GetBuyRecommendation2Async(symbol),
Stubs.GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyRedundancy(symbol As String) As Task
Dim recommendations As New List(Of Task(Of Boolean)) From {
Stubs.GetBuyRecommendation1Async(symbol),
Stubs.GetBuyRecommendation2Async(symbol),
Stubs.GetBuyRecommendation3Async(symbol)
}
Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
If Await recommendation Then Stubs.BuyStock(symbol)
End Function
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:
public static async Task WhenAnyRetryOnException(string symbol)
{
Task<bool>[] allRecommendations = new Task<bool>[]
{
Stubs.GetBuyRecommendation1Async(symbol),
Stubs.GetBuyRecommendation2Async(symbol),
Stubs.GetBuyRecommendation3Async(symbol)
};
var remaining = allRecommendations.ToList();
while (remaining.Count > 0)
{
Task<bool> recommendation = await Task.WhenAny(remaining);
try
{
if (await recommendation) Stubs.BuyStock(symbol);
break;
}
catch (WebException)
{
remaining.Remove(recommendation);
}
}
}
Public Async Function WhenAnyRetryOnException(symbol As String) As Task
Dim allRecommendations As Task(Of Boolean)() = {
Stubs.GetBuyRecommendation1Async(symbol),
Stubs.GetBuyRecommendation2Async(symbol),
Stubs.GetBuyRecommendation3Async(symbol)
}
Dim remaining As List(Of Task(Of Boolean)) = allRecommendations.ToList()
While remaining.Count > 0
Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(remaining)
Try
If Await recommendation Then Stubs.BuyStock(symbol)
Exit While
Catch ex As WebException
remaining.Remove(recommendation)
End Try
End While
End Function
Auch wenn eine erste Aufgabe 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. In diesem Szenario können Sie Fortsetzungen verwenden, um eine Benachrichtigung zu erhalten, wenn Aufgaben asynchron abgeschlossen werden:
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) { Stubs.Log(exc); }
}
}
Private Async Sub LogCompletionIfFailed(tasks As IEnumerable(Of Task))
For Each task In tasks
Try
Await task
Catch exc As Exception
Stubs.Log(exc)
End Try
Next
End Sub
Schließlich möchten Sie möglicherweise alle verbleibenden Vorgänge abbrechen:
public static async Task WhenAnyCancelRemainder(string symbol)
{
var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyCancelRemainder(symbol As String) As Task
Dim cts As New CancellationTokenSource()
Dim recommendations As New List(Of Task(Of Boolean)) From {
Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
}
Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
cts.Cancel()
If Await recommendation Then Stubs.BuyStock(symbol)
End Function
Ü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.
public static async Task WhenAnyInterleaving(string[] imageUrls)
{
List<Task<Bitmap>> imageTasks =
(from imageUrl in imageUrls select Stubs.GetBitmapAsync(imageUrl)).ToList();
while (imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
Console.WriteLine($"Got image: {image.Width}x{image.Height}");
}
catch { }
}
}
Public Async Function WhenAnyInterleaving(imageUrls As String()) As Task
Dim imageTasks As List(Of Task(Of Bitmap)) =
(From imageUrl In imageUrls Select Stubs.GetBitmapAsync(imageUrl)).ToList()
While imageTasks.Count > 0
Try
Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
imageTasks.Remove(imageTask)
Dim image As Bitmap = Await imageTask
Console.WriteLine($"Got image: {image.Width}x{image.Height}")
Catch
End Try
End While
End Function
Sie können auch Interleaving auf ein Szenario anwenden, das eine rechenintensive Verarbeitung auf ThreadPool der heruntergeladenen Bilder umfasst, zum Beispiel:
public static async Task WhenAnyInterleavingWithProcessing(string[] imageUrls)
{
List<Task<Bitmap>> imageTasks =
(from imageUrl in imageUrls
select Stubs.GetBitmapAsync(imageUrl)
.ContinueWith(t => Stubs.ConvertImage(t.Result))).ToList();
while (imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
Console.WriteLine($"Got image: {image.Width}x{image.Height}");
}
catch { }
}
}
Public Async Function WhenAnyInterleavingWithProcessing(imageUrls As String()) As Task
Dim imageTasks As List(Of Task(Of Bitmap)) =
(From imageUrl In imageUrls
Select Stubs.GetBitmapAsync(imageUrl).ContinueWith(Function(t) Stubs.ConvertImage(t.Result))).ToList()
While imageTasks.Count > 0
Try
Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
imageTasks.Remove(imageTask)
Dim image As Bitmap = Await imageTask
Console.WriteLine($"Got image: {image.Width}x{image.Height}")
Catch
End Try
End While
End Function
Drosselung
Betrachten Sie das Überlagerungsbeispiel, außer dass der Benutzer so viele Bilder herunterlädt, dass die Download-Geschwindigkeit gedrosselt werden muss. Sie möchten z. B. nur eine bestimmte Anzahl von Downloads gleichzeitig ausführen. Um dieses Ziel zu erreichen, starten Sie eine Teilmenge der asynchronen Vorgänge. Wenn Vorgänge abgeschlossen sind, können Sie zusätzliche Vorgänge starten, um deren Platz einzunehmen.
public static async Task WhenAnyThrottling(Uri[] uriList)
{
const int CONCURRENCY_LEVEL = 15;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while (nextIndex < CONCURRENCY_LEVEL && nextIndex < uriList.Length)
{
imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
nextIndex++;
}
while (imageTasks.Count > 0)
{
try
{
Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
imageTasks.Remove(imageTask);
Bitmap image = await imageTask;
Console.WriteLine($"Got image: {image.Width}x{image.Height}");
}
catch (Exception exc) { Stubs.Log(exc); }
if (nextIndex < uriList.Length)
{
imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
nextIndex++;
}
}
}
Public Async Function WhenAnyThrottling(uriList As Uri()) As Task
Const CONCURRENCY_LEVEL As Integer = 15
Dim nextIndex As Integer = 0
Dim imageTasks As New List(Of Task(Of Bitmap))
While nextIndex < CONCURRENCY_LEVEL AndAlso nextIndex < uriList.Length
imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
nextIndex += 1
End While
While imageTasks.Count > 0
Try
Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
imageTasks.Remove(imageTask)
Dim image As Bitmap = Await imageTask
Console.WriteLine($"Got image: {image.Width}x{image.Height}")
Catch exc As Exception
Stubs.Log(exc)
End Try
If nextIndex < uriList.Length Then
imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
nextIndex += 1
End If
End While
End Function
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:
class EarlyBailoutUI
{
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();
try
{
Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url");
await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
if (imageDownload.IsCompleted)
{
Bitmap image = await imageDownload;
Stubs.Log(image);
}
else imageDownload.ContinueWith(t => Stubs.Log(t));
}
finally { }
}
}
Class EarlyBailoutUI
Private m_cts As CancellationTokenSource
Public Sub btnCancel_Click(sender As Object, e As EventArgs)
If m_cts IsNot Nothing Then m_cts.Cancel()
End Sub
Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
m_cts = New CancellationTokenSource()
Try
Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url")
Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
If imageDownload.IsCompleted Then
Dim image As Bitmap = Await imageDownload
Stubs.Log(image)
Else
imageDownload.ContinueWith(Sub(t) Stubs.Log(t))
End If
Finally
End Try
End Sub
End Class
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:
class EarlyBailoutWithTokenUI
{
private CancellationTokenSource? m_cts;
public async void btnRun_Click(object sender, EventArgs e)
{
m_cts = new CancellationTokenSource();
try
{
Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url", m_cts.Token);
await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
Bitmap image = await imageDownload;
Stubs.Log(image);
}
catch (OperationCanceledException) { }
finally { }
}
}
Class EarlyBailoutWithTokenUI
Private m_cts As CancellationTokenSource
Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
m_cts = New CancellationTokenSource()
Try
Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url", m_cts.Token)
Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
Dim image As Bitmap = Await imageDownload
Stubs.Log(image)
Catch ex As OperationCanceledException
Finally
End Try
End Sub
End Class
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
Verwenden Sie die Task.Delay Methode, um Pausen zur Ausführung einer asynchronen Methode hinzuzufügen. Diese Pause ist nützlich für viele Arten von Funktionalitäten, einschließlich dem Erstellen von Abrufschleifen und der Verzögerung der Behandlung von Benutzereingaben für einen vordefinierten Zeitraum. Sie können auch die Methode Task.Delay mit Task.WhenAny verwenden, um Timeouts für Wartevorgänge 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. Verwenden Sie stattdessen Task.Delay und Task.WhenAny gemeinsam, um ein Timeout einzusetzen.
Nehmen Sie 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 static async Task<Bitmap?> DownloadWithTimeout(string url)
{
Task<Bitmap> download = Stubs.GetBitmapAsync(url);
if (download == await Task.WhenAny(download, Task.Delay(3000)))
{
return await download;
}
else
{
var ignored = download.ContinueWith(
t => Trace($"Task finally completed: {t.Status}"));
return null;
}
}
static void Trace(string message) => Console.WriteLine(message);
Public Async Function DownloadWithTimeout(url As String) As Task(Of Bitmap)
Dim download As Task(Of Bitmap) = Stubs.GetBitmapAsync(url)
If download Is Await Task.WhenAny(download, Task.Delay(3000)) Then
Return Await download
Else
Dim ignored = download.ContinueWith(Sub(t) TraceMsg($"Task finally completed: {t.Status}"))
Return Nothing
End If
End Function
Das gleiche Prinzip gilt für mehrere Downloads, da WhenAll eine Aufgabe zurückgibt.
public static async Task<Bitmap[]?> DownloadMultipleWithTimeout(string[] imageUrls)
{
Task<Bitmap[]> downloads =
Task.WhenAll(from url in imageUrls select Stubs.GetBitmapAsync(url));
if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
{
return await downloads;
}
else
{
downloads.ContinueWith(t => Stubs.Log(t));
return null;
}
}
Public Async Function DownloadMultipleWithTimeout(imageUrls As String()) As Task(Of Bitmap())
Dim downloads As Task(Of Bitmap()) =
Task.WhenAll(From url In imageUrls Select Stubs.GetBitmapAsync(url))
If downloads Is Await Task.WhenAny(downloads, Task.Delay(3000)) Then
Return Await downloads
Else
downloads.ContinueWith(Sub(t) Stubs.Log(t))
Return Nothing
End If
End Function
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 möchten Sie einen Vorgang wiederholen, wenn ein vorheriger Versuch fehlschlägt. Für synchronen Code können Sie eine Hilfsmethode erstellen, wie z. B. RetryOnFault im folgenden Beispiel, um diese Aufgabe auszuführen.
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)!;
}
Public Function RetryOnFaultSync(Of T)(func As Func(Of T), maxTries As Integer) As T
For i As Integer = 0 To maxTries - 1
Try
Return func()
Catch
If i = maxTries - 1 Then Throw
End Try
Next
Return Nothing
End Function
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)!;
}
Public Async Function RetryOnFault(Of T)(func As Func(Of Task(Of T)), maxTries As Integer) As Task(Of T)
For i As Integer = 0 To maxTries - 1
Try
Return Await func().ConfigureAwait(False)
Catch
If i = maxTries - 1 Then Throw
End Try
Next
Return Nothing
End Function
Anschließend können Sie diese Kombination verwenden, um Wiederholungen in der Logik der Anwendung zu codieren. 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> akzeptieren, die von der Funktion zwischen Wiederholungen aufgerufen wird, um zu bestimmen, wann der Vorgang erneut versucht werden soll. Beispiel:
public static async Task<T> RetryOnFaultWithDelay<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)!;
}
Public Async Function RetryOnFaultWithDelay(Of T)(
func As Func(Of Task(Of T)), maxTries As Integer, retryWhen As Func(Of Task)) As Task(Of T)
For i As Integer = 0 To maxTries - 1
Try
Return Await func().ConfigureAwait(False)
Catch
If i = maxTries - 1 Then Throw
End Try
Await retryWhen().ConfigureAwait(False)
Next
Return Nothing
End Function
Anschließend können Sie 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 unterschiedlichen Tageszeiten bieten jeder Dienst möglicherweise unterschiedliche Qualitäts- und Antwortzeiten. 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<T>(
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 => Stubs.Log(t), TaskContinuationOptions.OnlyOnFaulted);
}
return await completed;
}
Public Async Function NeedOnlyOne(Of T)(
ParamArray functions As Func(Of CancellationToken, Task(Of T))()) As Task(Of T)
Dim cts As New CancellationTokenSource()
Dim tasks As Task(Of T)() = (From func In functions Select func(cts.Token)).ToArray()
Dim completed As Task(Of T) = Await Task.WhenAny(tasks).ConfigureAwait(False)
cts.Cancel()
For Each task In tasks
Dim ignored = task.ContinueWith(
Sub(tsk) Stubs.Log(tsk), TaskContinuationOptions.OnlyOnFaulted)
Next
Return Await completed
End Function
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));
Verflochtene Vorgänge
Die Verwendung der WhenAny Methode zur Unterstützung eines Interoperabilitätsszenarios kann zu einem Leistungsproblem führen, wenn Sie mit großen Aufgabengruppen arbeiten. Jeder Aufruf von WhenAny registriert eine Fortsetzung bei jeder Aufgabe. Bei N-Anzahl von Vorgängen erstellt dieser Prozess O(N2)-Fortsetzungen über die Lebensdauer des zwischengespeicherten Vorgangs. Wenn Sie mit einer großen Gruppe von Aufgaben arbeiten, verwenden Sie einen Kombinator (Interleaved im folgenden Beispiel), um das Leistungsproblem zu beheben:
public 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;
}
Public Function Interleaved(Of T)(tasks As IEnumerable(Of Task(Of T))) As IEnumerable(Of Task(Of T))
Dim inputTasks As List(Of Task(Of T)) = tasks.ToList()
Dim sources As List(Of TaskCompletionSource(Of T)) =
(From _i In Enumerable.Range(0, inputTasks.Count) Select New TaskCompletionSource(Of T)()).ToList()
Dim indexRef As Integer() = {-1}
For Each inputTask In inputTasks
inputTask.ContinueWith(Sub(completed)
Dim idx = Interlocked.Increment(indexRef(0))
Dim source = sources(idx)
If completed.IsFaulted Then
source.TrySetException(completed.Exception.InnerExceptions)
ElseIf completed.IsCanceled Then
source.TrySetCanceled()
Else
source.TrySetResult(completed.Result)
End If
End Sub,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default)
Next
Return From source In sources Select source.Task
End Function
Verwenden Sie den Kombinator, um die Ergebnisse von Vorgängen während der Ausführung zu verarbeiten. Beispiel:
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;
…
}
WhenAllOrFirstException
In bestimmten Verteilungs-/Sammelszenarien sollten Sie möglicherweise auf alle Vorgänge in einer Gruppe warten, es sei denn, einer von ihnen schlägt fehl. In diesem Fall möchten Sie nicht mehr warten, sobald die Ausnahme auftritt. Sie können dieses Verhalten mithilfe 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 => ((Task<T>)t).Result).ToArray());
};
foreach (var t in inputs) t.ContinueWith(onCompleted);
return tcs.Task;
}
Public Function WhenAllOrFirstException(Of T)(tasks As IEnumerable(Of Task(Of T))) As Task(Of T())
Dim inputs As List(Of Task(Of T)) = tasks.ToList()
Dim ce As New CountdownEvent(inputs.Count)
Dim tcs As New TaskCompletionSource(Of T())()
Dim onCompleted As Action(Of Task) = Sub(completed As Task)
If completed.IsFaulted Then
tcs.TrySetException(completed.Exception.InnerExceptions)
End If
If ce.Signal() AndAlso Not tcs.Task.IsCompleted Then
tcs.TrySetResult(inputs.Select(Function(taskItem) DirectCast(taskItem, Task(Of T)).Result).ToArray())
End If
End Sub
For Each t In inputs
t.ContinueWith(onCompleted)
Next
Return tcs.Task
End Function
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 einer Aufgabe ist, dass Sie sie mehreren Verbrauchern aushändigen können. Alle Verbraucher können sie erwarten, Fortsetzungen dafür registrieren, ihr Ergebnis oder Ausnahmen (im Falle von Task<TResult>) usw. erhalten. Task und Task<TResult> eignen sich hervorragend für den Einsatz in einer asynchronen Zwischenspeicherungsinfrastruktur. Hier ist ein Beispiel für einen kleinen, aber leistungsstarken asynchronen Cache, der auf folgendem Task<TResult>Beispiel basiert:
public class AsyncCache<TKey, TValue> where TKey : notnull
{
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(nameof(valueFactory));
_valueFactory = valueFactory;
_map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
}
public Task<TValue> this[TKey key]
{
get
{
if (key == null) throw new ArgumentNullException(nameof(key));
return _map.GetOrAdd(key, toAdd =>
new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
}
}
}
Public Class AsyncCache(Of TKey, TValue)
Private ReadOnly _valueFactory As Func(Of TKey, Task(Of TValue))
Private ReadOnly _map As New ConcurrentDictionary(Of TKey, Lazy(Of Task(Of TValue)))()
Public Sub New(valueFactory As Func(Of TKey, Task(Of TValue)))
If valueFactory Is Nothing Then Throw New ArgumentNullException(NameOf(valueFactory))
_valueFactory = valueFactory
End Sub
Default Public ReadOnly Property Item(key As TKey) As Task(Of TValue)
Get
If key Is Nothing Then Throw New ArgumentNullException(NameOf(key))
Return _map.GetOrAdd(key, Function(toAdd) New Lazy(Of Task(Of TValue))(Function() _valueFactory(toAdd))).Value
End Get
End Property
End Class
Die AsyncCache<TKey,TValue-Klasse> akzeptiert als Delegat für seinen Konstruktor eine Funktion, die ein TKey nimmt und ein Task<TResult> zurückgibt. Das interne Wörterbuch speichert alle zuvor aus dem Cache zugegriffenen Werte und AsyncCache stellt sicher, dass nur eine Aufgabe pro Schlüssel generiert wird, auch 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.
static AsyncCache<string, string> m_webPages =
new AsyncCache<string, string>(url => Stubs.DownloadStringTaskAsync(url));
public static async Task UseWebPageCache(string url)
{
string contents = await m_webPages[url];
Console.WriteLine(contents.Length);
}
Private m_webPages As New AsyncCache(Of String, String)(Function(url) Stubs.DownloadStringTaskAsync(url))
Public Async Function UseWebPageCache(url As String) As Task
Dim contents As String = Await m_webPages(url)
Console.WriteLine(contents.Length)
End Function
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 Verbraucher nutzen, und die Produzenten und Verbraucher können gleichzeitig laufen. Beispielsweise verarbeitet der Verbraucher Artikel 1, der zuvor von einem Hersteller generiert wurde, der jetzt Artikel 2 produziert. Für das Produzenten-/Verbrauchermuster benötigen Sie immer eine Datenstruktur, um die von den Herstellern erstellten Arbeiten zu speichern, damit die Verbraucher über neue Daten benachrichtigt werden und sie bei Bedarf finden können.
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;
}
}
}
}
Public Class AsyncProducerConsumerCollection(Of T)
Private ReadOnly m_collection As New Queue(Of T)()
Private ReadOnly m_waiting As New Queue(Of TaskCompletionSource(Of T))()
Public Sub Add(item As T)
Dim tcs As TaskCompletionSource(Of T) = Nothing
SyncLock m_collection
If m_waiting.Count > 0 Then
tcs = m_waiting.Dequeue()
Else
m_collection.Enqueue(item)
End If
End SyncLock
If tcs IsNot Nothing Then tcs.TrySetResult(item)
End Sub
Public Function Take() As Task(Of T)
SyncLock m_collection
If m_collection.Count > 0 Then
Return Task.FromResult(m_collection.Dequeue())
Else
Dim tcs As New TaskCompletionSource(Of T)()
m_waiting.Enqueue(tcs)
Return tcs.Task
End If
End SyncLock
End Function
End Class
Mit dieser Datenstruktur können Sie Code wie die folgenden schreiben:
static AsyncProducerConsumerCollection<int> m_data = new();
public static async Task ConsumerAsync()
{
while (true)
{
int nextItem = await m_data.Take();
Stubs.ProcessNextItem(nextItem);
}
}
public static void Produce(int data)
{
m_data.Add(data);
}
Private m_data As New AsyncProducerConsumerCollection(Of Integer)()
Public Async Function ConsumerAsync() As Task
While True
Dim nextItem As Integer = Await m_data.Take()
Stubs.ProcessNextItem(nextItem)
End While
End Function
Public Sub Produce(data As Integer)
m_data.Add(data)
End Sub
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.
static BufferBlock<int> m_dataBlock = new();
public static async Task ConsumerAsyncBlock()
{
while (true)
{
int nextItem = await m_dataBlock.ReceiveAsync();
Stubs.ProcessNextItem(nextItem);
}
}
public static void ProduceBlock(int data)
{
m_dataBlock.Post(data);
}
Private m_dataBlock As New BufferBlock(Of Integer)()
Public Async Function ConsumerAsyncBlock() As Task
While True
Dim nextItem As Integer = Await m_dataBlock.ReceiveAsync()
Stubs.ProcessNextItem(nextItem)
End While
End Function
Public Sub ProduceBlock(data As Integer)
m_dataBlock.Post(data)
End Sub
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.