Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Při práci s asynchronními operacemi pomocí asynchronního vzoru založeného na úlohách (TAP) můžete pomocí zpětných volání dosáhnout čekání bez blokování. Pro úlohy tento vzor používá metody, jako je Task.ContinueWith. Asynchronní podpora založená na jazyce skryje zpětná volání tím, že umožňuje, aby se asynchronní operace čekaly v rámci normálního toku řízení, a kód generovaný kompilátorem poskytuje stejnou podporu na úrovni rozhraní API.
Pozastavení spuštění pomocí příkazu Await
Pomocí klíčového slova await v jazyce C# a operátoru Await v jazyce Visual Basic můžete asynchronně očekávat Task a Task<TResult> objekty. Když očekáváte výraz Task, await je typu void. Když očekáváte výraz Task<TResult>, await je typu TResult. Výraz await musí nastat uvnitř těla asynchronní metody. (Tyto jazykové funkce byly zavedeny v rozhraní .NET Framework 4.5.)
V zákulisí funkce await nainstaluje zpětné volání na úlohu pomocí pokračování operací. Toto zpětné volání obnoví asynchronní metodu v okamžiku pozastavení. Když je asynchronní metoda obnovena, pokud očekávaná operace byla úspěšně dokončena a byla typu Task<TResult>, pak je vrácena její hodnota TResult. Pokud byla očekávaná hodnota Task ukončena ve Task<TResult> stavu, je vyvolána Canceled výjimka. Pokud Task nebo Task<TResult>, které bylo očekáváno, skončilo ve stavu Faulted, je vyvolána výjimka, která způsobila jeho selhání. V Task může dojít k selhání v důsledku několika výjimečných stavů, ale pouze jedna z těchto výjimek je šířena dál. Vlastnost Task.Exception však vrátí AggregateException výjimku, která obsahuje všechny chyby.
Pokud je kontext synchronizace (SynchronizationContext objekt) přidružený k vláknu, které spouštělo asynchronní metodu v době pozastavení (například pokud SynchronizationContext.Current vlastnost není null), asynchronní metoda pokračuje ve stejném kontextu synchronizace pomocí metody kontextu Post . V opačném případě závisí na plánovači úloh (TaskScheduler objektu), který byl aktuální v době pozastavení. Obvykle se jedná o výchozí plánovač úloh (TaskScheduler.Default), který cílí na fond vláken. Tento plánovač úloh určuje, jestli má očekávaná asynchronní operace pokračovat tam, kde byla dokončena, nebo zda má být naplánováno obnovení. Výchozí plánovač obvykle umožňuje pokračování spustit ve vlákně, ve kterém byla dokončena očekávaná operace.
Když voláte asynchronní metodu, synchronně spustí tělo funkce až do prvního výrazu await na čekající instanci, která ještě není dokončená, a poté se volání vrátí volajícímu. Pokud asynchronní metoda nevrací void, vrátí Task hodnotu nebo Task<TResult> objekt představující probíhající výpočty. V nevoidové asynchronní metodě, pokud je zjištěn návratový příkaz nebo je dosaženo konce těla metody, je úkol dokončen v RanToCompletion konečném stavu. Pokud neošetřená výjimka způsobí, že ovládací prvek opustí tělo asynchronní metody, úloha skončí ve Faulted stavu. Pokud je touto výjimkou OperationCanceledException, úkol místo toho končí ve stavu Canceled. Tímto způsobem se výsledek nebo výjimka nakonec publikuje.
Existuje několik důležitých variant tohoto chování. Z důvodu výkonu, pokud je úkol již dokončený v době, kdy je úkol očekáván, řízení se nepředá a funkce pokračuje ve vykonávání. Kromě toho návrat do původního kontextu není vždy požadované chování a lze ho změnit; toto chování je podrobněji popsáno v další části.
Konfigurace pozastavení a obnovení s využitím yield a ConfigureAwait
Několik metod poskytuje větší kontrolu nad prováděním asynchronní metody. Metodu Task.Yield můžete například použít k zavedení bodu výnosu do asynchronní metody:
public class Task : …
{
public static YieldAwaitable Yield();
…
}
Tato metoda je ekvivalentní asynchronnímu postování nebo plánování do aktuálního kontextu.
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
Můžete také použít metodu Task.ConfigureAwait pro lepší kontrolu nad pozastavením a obnovením v asynchronní metodě. Jak už jsme zmínili dříve, aktuální kontext je zachycen v době pozastavení asynchronní metody a tento zachycený kontext se používá k vyvolání pokračování asynchronní metody při obnovení. V mnoha případech se jedná o přesné chování, které chcete. V jiných případech vás nemusí zajímat kontext pokračování a můžete dosáhnout lepšího výkonu tím, že se vyhnete návratu do původního kontextu. Chcete-li toto chování povolit, použijte metodu Task.ConfigureAwait k informování operace await, aby nezachytila a neobnovila kontext, ale aby pokračovala v provádění v místě, kde byla dokončena asynchronní operace, která byla očekávána.
await someTask.ConfigureAwait(continueOnCapturedContext:false);
Awaitables (čekatelné objekty), ConfigureAwait a SynchronizationContext
await funguje s libovolným typem, který splňuje očekávaný vzor výrazu, nejen Tasks . Typ je očekáván, pokud poskytuje kompatibilní GetAwaiter metodu, která vrací typ s IsCompleted, OnCompleteda GetResult členy. Ve většině veřejných rozhraní API, return Task, Task<TResult>, ValueTasknebo ValueTask<TResult>. Pro specializované scénáře používejte vlastní "awaitables".
Použijte ConfigureAwait , když pokračování nepotřebuje kontext volajícího. V kódu aplikace, který aktualizuje uživatelské rozhraní, se často vyžaduje zachytávání kontextu. V opakovaně použitelném kódu knihovny se obvykle upřednostňuje ConfigureAwait(false), protože zabraňuje zbytečným přepínáním kontextu a snižuje riziko zablokování pro volající, kteří způsobují blokování.
ConfigureAwait(false) upravuje plánování pokračování, nikoli ExecutionContext tok. Podrobnější vysvětlení chování kontextu naleznete v tématu ExecutionContext a SynchronizationContext.
Zrušení asynchronní operace
Metody TAP podporující zrušení poskytují alespoň jednu variantu přetížení, která přijímá token zrušení (CancellationToken objekt), počínaje rozhraním .NET Framework 4.
Token zrušení vytvoříte prostřednictvím zdroje tokenu zrušení (CancellationTokenSource objektu). Vlastnost zdroje Token vrátí token zrušení, který signalizuje, když je volána metoda zdroje Cancel .
var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();
Pokud například chcete stáhnout jednu webovou stránku a chcete operaci zrušit, vytvořte CancellationTokenSource objekt, předejte jeho token metodě TAP a potom zavolejte metodu zdroje Cancel , až budete připraveni operaci zrušit:
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();
Nebo můžete stejný token předat selektivní podmnožině operací:
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();
Důležité
Jakékoli vlákno může iniciovat žádosti o zrušení.
Hodnotu můžete předat CancellationToken.None jakékoli metodě, která přijímá token zrušení, což znamená, že zrušení se nikdy nepožaduje. Tato hodnota způsobí, že vlastnost CancellationToken.CanBeCanceled vrátí false, a volaná metoda může podle toho optimalizovat. Pro účely testování můžete také předat předem zrušený token zrušení, který je instancován pomocí konstruktoru, který přijímá boolovskou hodnotu označující, zda má token být v již zrušeném nebo nezrušitelném stavu.
Tento přístup ke zrušení má několik výhod:
Stejný token zrušení můžete předat libovolnému počtu asynchronních a synchronních operací.
Stejná žádost o zrušení může být doručena libovolnému počtu posluchačů.
Vývojář asynchronního rozhraní API má úplnou kontrolu nad tím, jestli je možné požadovat zrušení a kdy se projeví.
Kód, který využívá API, může selektivně určit asynchronní volání, na která se mají zaměřit požadavky na zrušení.
Monitorování průběhu
Některé asynchronní metody zpřístupňují průběh prostřednictvím rozhraní průběhu, které předáváte do asynchronní metody. Představte si například funkci, která asynchronně stáhne řetězec textu a zároveň zvyšuje průběh aktualizací, které zahrnují procento stahování, které se zatím dokončilo. Takovou metodu můžete použít v aplikaci Windows Presentation Foundation (WPF (Windows Presentation Foundation)) následujícím způsobem:
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; }
}
Použití integrovaných kombinátorů založených na úlohách
Obor System.Threading.Tasks názvů zahrnuje několik metod pro kompozici a práci s úkoly.
Poznámka:
Několik ukázek kódu v této části používá Bitmap, což vyžaduje balíček System.Drawing.Common a je podporován pouze na Windows. Asynchronní vzory, které demonstrují použití na všech platformách; pro cíle, které nejsou Windows, nahraďte knihovnu pro vytváření obrázků multiplatformní variantou.
Task.Run
Třída Task obsahuje několik Run metod, které vám umožňují snadno přesměrovat práci do fondu vláken jako Task nebo Task<TResult>. Například:
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
Některé z těchto Run metod, jako je například přetížení Task.Run(Func<Task>), existují jako zkratka pro metodu TaskFactory.StartNew. Toto přetížení umožňuje používat await při práci s přenesenou prací. Například:
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
Taková přetížení jsou logicky ekvivalentní použití TaskFactory.StartNew metody ve spojení s rozšiřující metodou Unwrap v Task Parallel Library.
Task.FromResult
Tuto metodu FromResult použijte ve scénářích, ve kterých jsou data možná k dispozici, a stačí je vrátit z metody vracející úlohu do Task<TResult>.
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 (metoda zajišťující simultánní provedení více úkolů)
WhenAll Pomocí metody můžete asynchronně čekat na více asynchronních operací, které jsou reprezentovány jako úlohy. Metoda má více přetížení, která podporují sadu negenerických úloh nebo nejednotnou sadu generických úloh (například asynchronní čekání na více operací, které vracejí void, nebo asynchronní čekání na více metod vracejících hodnotu, kde každá hodnota může mít jiný typ) a podporují jednotnou sadu generických úloh (například asynchronní čekání na více metod vracejících TResult).
Předpokládejme, že chcete poslat e-mailové zprávy několika zákazníkům. Odesílání zpráv můžete překrývat, takže před odesláním další zprávy nečekáte na dokončení jedné zprávy. Můžete také zjistit, kdy se operace odesílání dokončí a jestli dojde k nějakým chybám:
IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);
Tento kód explicitně nezpracovává výjimky, ke kterým může dojít, ale umožňuje výjimkám z await vyjít z výsledné úlohy z WhenAll. Ke zpracování výjimek použijte například následující kód:
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
V takovém případě, pokud jakákoli asynchronní operace selže, všechny výjimky jsou sloučeny v výjimce AggregateException , která je uložena v Task vrácené z WhenAll metody. Avšak klíčové slovo šíří pouze jednu z těchto výjimek await. Pokud chcete prozkoumat všechny výjimky, můžete předchozí kód přepsat následujícím způsobem:
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
Představte si příklad asynchronního stahování více souborů z webu. V tomto případě mají všechny asynchronní operace homogenní typy výsledků a snadno se k výsledkům dostanete:
string [] pages = await Task.WhenAll(
from url in urls select DownloadStringTaskAsync(url));
Můžete použít stejné techniky zpracování výjimek popsané v předchozím scénáři s návratem void.
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
WhenAny Pomocí metody můžete asynchronně počkat na dokončení pouze jedné z několika asynchronních operací reprezentovaných jako úkoly. Tato metoda slouží ke čtyřem primárním případům použití:
Redundance: Provedení operace několikrát a výběr první operace (například kontaktování více webových služeb burzovních nabídek, které vrací jeden výsledek a výběr toho, který se dokončí nejrychleji).
Prokládání: Spuštění více operací a čekání na dokončení všech, přičemž je každá zpracovávána postupně po jejím dokončení.
Omezování: Povolení zahájení dalších operací při dokončení jiných. Tento scénář je rozšířením scénáře prokládání.
Časná záchrana: Například operaci reprezentovanou úkolem t1 lze seskupit do WhenAny úkolu s jiným úkolem t2 a můžete počkat na WhenAny úkol. Úkol t2 může představovat časový limit, zrušení nebo jiný signál, který způsobí, že úkol WhenAny bude dokončen před dokončením úkolu t1.
Přebytečnost
Zvažte případ, kdy chcete učinit rozhodnutí o tom, jestli koupit akcie. Existuje několik webových služeb doporučení akcií, kterým důvěřujete, ale v závislosti na denním zatížení může každá služba být v různých časech pomalá. Metodu WhenAny použijte k přijetí oznámení po dokončení jakékoli operace:
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
Na rozdíl od WhenAlltoho, který vrátí rozbalené výsledky všech úkolů, které byly úspěšně dokončeny, WhenAny vrátí úkol, který byl dokončen. Pokud úkol selže, je důležité vědět, že selhal, a pokud je úkol úspěšný, je důležité vědět, ke kterému úkolu je přidružená vrácená hodnota. Proto potřebujete získat přístup k výsledku vráceného úkolu nebo ho dále očekávat, jak ukazuje tento příklad.
Stejně jako v případě WhenAll, musíte být schopni pojmout výjimky. Vzhledem k tomu, že se dokončený úkol vrátí zpět, můžete očekávat, že vrácený úkol bude mít chyby šířené a try/catch odpovídajícím způsobem. Například:
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
Navíc i v případě úspěšného dokončení prvního úkolu může dojít k selhání následných úloh. V tomto okamžiku máte několik možností pro řešení výjimek: Můžete počkat, až se všechny spuštěné úkoly dokončí, v takovém případě můžete použít metodu WhenAll , nebo se můžete rozhodnout, že všechny výjimky jsou důležité a musí být zaprotokolovány. V tomto scénáři můžete pomocí pokračování dostávat oznámení, když se úkoly dokončí asynchronně:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => { if (t.IsFaulted) Log(t.Exception); });
}
nebo:
foreach(Task recommendation in recommendations)
{
var ignored = recommendation.ContinueWith(
t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}
nebo dokonce:
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
Nakonec můžete chtít zrušit všechny zbývající operace:
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
Prokládání
Představte si případ, kdy stahujete obrázky z webu a zpracováváte jednotlivé obrázky (například přidání obrázku do ovládacího prvku uživatelského rozhraní). Obrázky zpracováváte postupně ve vlákně uživatelského rozhraní, ale chcete je stáhnout co nejparalelněji. Také nechcete, aby se obrázky přidávaly do uživatelského rozhraní, dokud se všechny nestáhnou. Namísto toho je chcete přidat až po jejich dokončení.
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
Můžete také použít prokládání ve scénáři zahrnujícím výpočetně náročné zpracování stažených ThreadPool obrazů; například:
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
Řízení výkonu/omezování rychlosti
Představte si příklad prokládání, s výjimkou toho, že uživatel stahuje tolik obrázků, že stahování musí být omezeno. Například chcete, aby souběžně probíhalo jenom určité množství stahování. Chcete-li tohoto cíle dosáhnout, spusťte podmnožinu asynchronních operací. Po dokončení operací můžete zahájit další operace, které je nahradí.
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
Předčasná finanční záchrana
Zvažte, že čekáte asynchronně na dokončení operace, zatímco současně reagujete na žádost uživatele o zrušení (například uživatel kliknul na tlačítko zrušit). Tento scénář ilustruje následující kód:
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
Tato implementace znovu povolí uživatelské rozhraní, jakmile se rozhodnete provést záchranu, ale nezruší základní asynchronní operace. Další alternativou by bylo zrušit čekající operace, když se rozhodnete ukončit, ale neobnovit uživatelské rozhraní, dokud se operace nedokončí; operace by mohly být předčasně ukončeny kvůli žádosti o zrušení.
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
Dalším příkladem předčasné záchrany je použití metody WhenAny ve spojení s metodou Delay, jak je popsáno v další části.
Task.Delay
Tuto metodu Task.Delay použijte k přidání pozastavení do provádění asynchronní metody. Toto pozastavení je užitečné pro mnoho druhů funkcí, včetně vytváření smyček dotazování a zpoždění zpracování uživatelského vstupu pro předem určené časové období. Metodu Task.Delay spolu s Task.WhenAny můžete použít také k implementaci časových limitů při čekání.
Pokud dokončení úlohy, která je součástí větší asynchronní operace (například webové služby ASP.NET), trvá příliš dlouho, mohlo by to negativně ovlivnit celou operaci, zejména pokud by se nikdy nepodařilo úlohu dokončit. Z tohoto důvodu je důležité mít možnost nastavit časový limit při čekání na asynchronní operaci. Synchronní Task.Wait, Task.WaitAlla Task.WaitAny metody přijímají hodnoty časového limitu, ale odpovídající TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny a dříve uvedené Task.WhenAll/Task.WhenAny metody ne. Místo toho použijte Task.Delay a Task.WhenAny společně, abyste implementovali časový limit.
Předpokládejme například, že v aplikaci uživatelského rozhraní chcete stáhnout obrázek a během stahování obrázku uživatelské rozhraní zakázat. Pokud však stahování trvá příliš dlouho, chcete uživatelské rozhraní znovu povolit a zrušit stahování:
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
Stejný princip platí pro více stahování, protože WhenAll vrací úkol:
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
Vytváření kombinátorů založených na úlohách
Vzhledem k tomu, že úloha dokáže zcela reprezentovat asynchronní operaci a poskytovat synchronní a asynchronní funkce pro spojení s operací, načítání výsledků atd., můžete vytvořit užitečné knihovny kombinátorů, které vytvářejí úlohy pro vytváření větších vzorů. Jak je popsáno v předchozí části, .NET obsahuje několik předdefinovaných kombinátorů, ale můžete také vytvořit vlastní. Následující části obsahují několik příkladů možných metod a typů kombinátoru.
RetryOnFault
V mnoha situacích chcete operaci zopakovat, pokud předchozí pokus selže. Pro synchronní kód můžete vytvořit pomocnou metodu, jako RetryOnFault je například v následujícím příkladu, abyste mohli tuto úlohu provést:
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
Můžete vytvořit téměř identickou pomocnou metodu pro asynchronní operace, které jsou implementovány pomocí TAP, a tudíž vrátit úlohy:
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
Tento kombinátor pak můžete použít ke kódování opakovaných pokusů do logiky aplikace. Například:
// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
() => DownloadStringTaskAsync(url), 3);
Funkci můžete dále rozšířit RetryOnFault . Funkce může například přijmout jinou Func<Task> , kterou funkce vyvolá mezi opakovanými pokusy, aby určila, kdy se má operace opakovat. Například:
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
Funkci pak můžete použít následujícím způsobem, abyste před opakováním operace čekali na sekundu:
// 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
Někdy můžete využít redundanci ke zlepšení latence operace a šanci na úspěch. Vezměte v úvahu několik webových služeb, které poskytují kurzy akcií, ale v různých denních časech může každá služba poskytovat různé úrovně kvality a doby odezvy. Pokud chcete tyto výkyvy vyřešit, můžete vydávat požadavky na všechny webové služby a jakmile dostanete odpověď od jedné, zrušíte zbývající žádosti. Pomocnou funkci můžete implementovat, abyste usnadnili implementaci tohoto běžného vzoru spouštění více operací, čekání na jakoukoli operaci a zrušení zbytku. Funkce NeedOnlyOne v následujícím příkladu ilustruje tento scénář.
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
Tuto funkci pak můžete použít následujícím způsobem:
double currentPrice = await NeedOnlyOne(
ct => GetCurrentPriceFromServer1Async("msft", ct),
ct => GetCurrentPriceFromServer2Async("msft", ct),
ct => GetCurrentPriceFromServer3Async("msft", ct));
Prokládané operace
Použití metody WhenAny pro podporu scénáře prokládání může způsobit problém s výkonem při práci s velkými sadami úloh. Každé volání WhenAny registruje pokračování u každého úkolu. U N počtu úkolů tento proces vytvoří pokračování O(N2) po celou dobu životnosti operace prokládání. Pokud pracujete s velkou sadou úloh, použijte kombinátor (Interleaved v následujícím příkladu) k vyřešení problému s výkonem:
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
Kombinátor slouží ke zpracování výsledků úkolů při jejich dokončení. Například:
IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
int result = await task;
…
}
WhenAllOrFirstException
V určitých scénářích scatter/gather můžete chtít čekat na všechny úkoly v sadě, ledaže by některý z nich selhal. V takovém případě chcete přestat čekat, jakmile dojde k výjimce. Toto chování můžete dosáhnout pomocí metody kombinátoru, například WhenAllOrFirstException v následujícím příkladu:
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
Vytváření datových struktur založených na úlohách
Kromě schopnosti vytvářet vlastní kombinátory založené na úlohách má datová struktura v Task a Task<TResult> sílu díky tomu, že představuje jak výsledky asynchronní operace, tak i nutnou synchronizaci pro spojení s ní. Díky tomu ji lze považovat za mocný typ pro vytváření vlastních datových struktur, které se využijí v asynchronních scénářích.
AsyncCache
Jedním z důležitých aspektů úkolu je, že ho můžete předat více příjemcům. Všichni uživatelé na něj mohou čekat, registrovat pokračování, získat jeho výsledek nebo výjimky (v případě Task<TResult>) atd. Tento aspekt činí Task a Task<TResult> dokonale vhodnými pro použití v asynchronní infrastruktuře pro ukládání do mezipaměti. Tady je příklad malé, ale výkonné asynchronní mezipaměti založené na Task<TResult>:
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
AsyncCache<TKey,TValue> třída přijímá jako delegát do svého konstruktoru funkci, která přebírá TKey a vrací Task<TResult>. Interní slovník ukládá všechny dříve přístupné hodnoty z mezipaměti a AsyncCache zajišťuje, že vygeneruje pouze jeden úkol na klíč, i když se k mezipaměti přistupuje souběžně.
Můžete například vytvořit mezipaměť pro stažené webové stránky:
private AsyncCache<string,string> m_webPages =
new AsyncCache<string,string>(DownloadStringTaskAsync);
Tuto mezipaměť pak můžete použít v asynchronních metodách, kdykoli potřebujete obsah webové stránky. Třída AsyncCache zajišťuje, že stahujete co nejvíce stránek a ukládá výsledky do mezipaměti.
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
Úlohy můžete použít také k vytváření datových struktur pro koordinaci asynchronních aktivit. Zvažte jeden z klasických vzorů paralelního návrhu: producent/spotřebitel. V tomto modelu producenti generují data, která spotřebitelé spotřebovávají, a producenti a spotřebitelé mohou běžet paralelně. Příjemce například zpracovává položku 1, která byla dříve vygenerována producentem, který nyní vyrábí položku 2. Pro model producenta/spotřebitele vždy potřebujete k uložení práce vytvořené producenty určitou datovou strukturu, aby spotřebitelé mohli být upozorněni na nová data a najít je, pokud jsou k dispozici.
Tady je jednoduchá datová struktura založená na úlohách, která umožňuje použití asynchronních metod jako producent a spotřebitel:
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
S touto datovou strukturou můžete napsat například následující kód:
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
Obor System.Threading.Tasks.Dataflow názvů zahrnuje BufferBlock<T> typ, který můžete použít podobným způsobem, ale bez nutnosti sestavovat vlastní typ kolekce:
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
Poznámka:
Prostor názvů System.Threading.Tasks.Dataflow je k dispozici jako balíček NuGet. Pokud chcete nainstalovat sestavení, které obsahuje System.Threading.Tasks.Dataflow obor názvů, otevřete projekt v prostředí Visual Studio, v nabídce Projekt zvolte Spravovat balíčky NuGet a vyhledejte online balíček System.Threading.Tasks.Dataflow.