Sdílet prostřednictvím


Použití asynchronního vzoru založeného na úloze

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í. U úkolů se toho dosahuje prostřednictvím metod, jako Task.ContinueWithje . 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ž čekáte na , Taskawait výraz je typu void. Když čekáte na , Task<TResult>await výraz je typu TResult. Výraz await musí nastat uvnitř těla asynchronní metody. (Tyto jazykové funkce byly zavedeny v rozhraní .NET Framework 4.5.)

Pod kryty funkce await nainstaluje zpětné volání na úlohu pomocí pokračování. 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 Task<TResult>, je TResult vrácena. Task Pokud byla očekávaná hodnota Task<TResult> ukončena ve Canceled stavu, OperationCanceledException vyvolá se výjimka. Task Pokud došlo k ukončení nebo Task<TResult> očekávaného Faulted stavu, vyvolá se výjimka, která způsobila chybu. V Task důsledku několika výjimek může dojít k chybě, ale rozšíří se pouze jedna z těchto výjimek. 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í běžet ve vlákně, které byla dokončena očekávaná operace.

Když je volána asynchronní metoda, synchronně spustí tělo funkce až do prvního výrazu await v čekající instanci, která ještě nebyla dokončena, v tomto okamžiku se vyvolání vrátí volajícímu. Pokud asynchronní metoda nevrací void, vrátí se Task objekt nebo Task<TResult> představuje probíhající výpočet. Pokud je v asynchronní metodě, která není void, je-li zjištěn návratový příkaz nebo je dosaženo konce těla metody, úkol je 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 tato výjimka , OperationCanceledExceptionúkol místo toho končí ve Canceled stavu. 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ůvodů výkonu platí, že pokud již úkol dokončil v době, kdy je úkol očekávána, řízení se nevyvolá a funkce bude pokračovat v provádění. Kromě toho návrat do původního kontextu není vždy požadované chování a lze ho změnit; toto je podrobněji popsáno v další části.

Konfigurace pozastavení a obnovení s využitím výnosu a konfigurace

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();
    …
}

To je ekvivalentem asynchronního účtování nebo plánování zpět do aktuálního kontextu.

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

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 se nemusíte starat o kontext pokračování a můžete dosáhnout lepšího výkonu tím, že se těmto příspěvkům zabráníte zpět do původního kontextu. Pokud to chcete povolit, použijte metodu Task.ConfigureAwait k informování operace await, aby nezachytávala a neobnovovala v kontextu, ale chcete pokračovat v provádění bez ohledu na to, kde byla dokončena asynchronní operace, která byla očekávána:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Zrušení asynchronní operace

Počínaje rozhraním .NET Framework 4 poskytují metody TAP, které podporují zrušení, alespoň jedno přetížení, které přijímá token zrušení (CancellationToken objekt).

Token zrušení se vytvoří prostřednictvím zdroje tokenu zrušení (CancellationTokenSource objektu). Vlastnost zdroje Token vrátí token zrušení, který bude signalován při zavolání metody zdroje Cancel . Pokud například chcete stáhnout jednu webovou stránku a chcete operaci zrušit, vytvoříte CancellationTokenSource objekt, předáte jeho token metodě TAP a potom zavoláte metodu zdroje Cancel , až budete připraveni operaci zrušit:

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

Pokud chcete zrušit více asynchronních vyvolání, můžete předat stejný token všem vyvoláním:

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é

Žádosti o zrušení mohou být inicializovány z libovolného vlákna.

Hodnotu můžete předat CancellationToken.None jakékoli metodě, která přijímá token zrušení, což znamená, že zrušení nebude nikdy požadováno. To způsobí, že CancellationToken.CanBeCanceled vlastnost vrátit false, a volaná metoda může odpovídajícím způsobem optimalizovat. Pro účely testování můžete také předat předem zrušený token zrušení, který se vytvoří instance pomocí konstruktoru, který přijímá logickou hodnotu označující, jestli se má token spustit 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í se může prodloužit na libovolný počet naslouchacích procesů.

  • Vývojář asynchronního rozhraní API má úplnou kontrolu nad tím, jestli může být požadováno zrušení a kdy se může projevit.

  • Kód, který využívá rozhraní API, může selektivně určit asynchronní vyvolání, na které se budou požadavky zrušení šířit.

Sledování průběhu

Některé asynchronní metody zpřístupňují průběh prostřednictvím rozhraní průběhu předávaného 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. Taková metoda by mohla být využita v aplikaci WINDOWS Presentation Foundation (WPF) 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ů obsahuje několik metod pro psaní a práci s úkoly.

Task.Run

Třída Task obsahuje několik Run metod, které umožňují snadnou práci jako Task fond vláken nebo Task<TResult> do fondu vláken, například:

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

Některé z těchto Run metod, například Task.Run(Func<Task>) přetížení, existují jako zkratka pro metodu TaskFactory.StartNew . Toto přetížení umožňuje použít příkaz await v rámci práce s přesměrováním zatížení, například:

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

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

Použijte metodu FromResult ve scénářích, ve kterých již mohou být data k dispozici, a stačí je vrátit z metody Task<TResult>vracející úlohu do :

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

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

Task.WhenAll

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 ne generických úloh nebo ne uniformní sadu obecných úloh (například asynchronně čeká na více operací vracejících void nebo asynchronně čeká na více metod vracejících hodnotu, kde každá hodnota může mít jiný typ) a podporuje jednotnou sadu obecných úloh (například asynchronně čeká na více TResultmetod vrácení).

Řekněme, ž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čily a jestli došlo k nějakým chybám:

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

Tento kód explicitně nezpracuje výjimky, které mohou nastat, ale umožňuje rozšířit výjimky z await výsledné úlohy z WhenAll. K zpracování výjimek můžete použít například následující kód:

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

V takovém případě dojde-li k selhání jakékoli asynchronní operace, všechny výjimky budou sloučeny do AggregateException výjimky, která je uložena v Task vrácené z WhenAll metody. Klíčové slovo však šíří await pouze jednu z těchto výjimek. Pokud chcete prozkoumat všechny výjimky, můžete předchozí kód přepsat následujícím způsobem:

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

Podívejme se na 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, které jsme probrali v předchozím scénáři vrácení void:

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

Task.WhenAny

Metodu WhenAny můžete použít k asynchronnímu čekání pouze na jednu z několika asynchronních operací reprezentovaných jako úkoly k dokončení. 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é vytvoří jeden výsledek a vybere ten, který se dokončí nejrychleji).

  • Prokládání: Spuštění více operací a čekání na dokončení všech operací, ale zpracování během jejich dokončení.

  • Omezování: Povolení dalších operací, aby se začaly s dokončením dalších operací. Toto je rozšíření 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 nebo zrušení nebo jiný signál, který způsobí WhenAny dokončení úkolu před dokončením t1.

Redundance

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 můžete použít k přijetí oznámení po dokončení jakékoli operace:

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

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:

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

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 dokončí všechny spuštěné úkoly, 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 takovém případě 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) { Log(exc); }
    }
}
…
LogCompletionIfFailed(recommendations);

Nakonec můžete chtít zrušit všechny zbývající operace:

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

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

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 obrázky stáhnout co nejsouběrněji. Také nechcete, aby se obrázky přidávaly do uživatelského rozhraní, dokud se všechny nestáhnou. Místo toho je chcete přidat po dokončení.

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

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

Můžete také použít prokládání ve scénáři, který zahrnuje výpočetně náročné zpracování stažených ThreadPool imagí, například:

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

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

Omezování

Představte si příklad prokládání, s výjimkou toho, že uživatel stahuje tolik obrázků, které musí stahování omezovat; Například chcete, aby souběžně probíhal pouze určitý počet stahování. Abyste toho dosáhli, můžete spustit podmnožinu asynchronních operací. Po dokončení operací můžete zahájit další operace, které se mají provést:

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

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

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

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

Předčasné výpomoci

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:

private CancellationTokenSource m_cts;

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

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

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

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šení čekajících operací při rozhodování o záchraně, ale ne k obnovení uživatelského rozhraní, dokud se operace nedokončí, potenciálně kvůli předčasnému ukončení kvůli žádosti o zrušení:

private CancellationTokenSource m_cts;

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

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

Dalším příkladem předčasného záchrany je použití WhenAny metody ve spojení s metodou Delay , jak je popsáno v další části.

Task.Delay

Tuto metodu Task.Delay můžete použít k zavedení pozastavení do provádění asynchronní metody. To je užitečné pro mnoho druhů funkcí, včetně vytváření cyklů dotazování a zpoždění zpracování uživatelského vstupu pro předem určené časové období. Tato Task.Delay metoda může být užitečná také v kombinaci s Task.WhenAny implementací časových limitů pro operátory await.

Pokud dokončení úlohy, která je součástí větší asynchronní operace (například webová služba ASP.NET), může celková operace trvat příliš dlouho, zejména pokud se nepodaří dokončit. Z tohoto důvodu je důležité mít při čekání na asynchronní operaci časový limit. Synchronní Task.Waita Task.WaitAllTask.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 můžete použít Task.Delay kombinaci Task.WhenAny k implementaci časového limitu.

Řekněme například, že v aplikaci uživatelského rozhraní chcete stáhnout obrázek a zakázat uživatelské rozhraní při stahování obrázku. Pokud však stahování trvá příliš dlouho, chcete uživatelské rozhraní znovu povolit a zrušit stahování:

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

Totéž platí pro více stažení, protože WhenAll vrací úlohu:

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

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 můžete chtít operaci zopakovat, pokud předchozí pokus selže. Pro synchronní kód můžete vytvořit pomocnou metodu, například RetryOnFault v následujícím příkladu, abyste toho dosáhli:

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);
}

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);
}

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 další Func<Task> , která se vyvolá mezi opakovanými pokusy, aby určila, kdy operaci zkusit znovu. Například:

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

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. Tento NeedOnlyOne scénář ilustruje funkce v následujícím příkladu:

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

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ádání operací

Při práci s velkými sadami úloh může dojít k potenciálnímu problému s výkonem při použití WhenAny metody prokládání. Každé volání má WhenAny za následek registraci pokračování u každého úkolu. U N počtu úkolů to vede k pokračování O(N2) vytvořených v průběhu životnosti operace prokládání. Pokud pracujete s velkou sadou úloh, můžete problém s výkonem vyřešit pomocí kombinátoru (Interleaved v následujícím příkladu):

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;
}

Kombinátor pak můžete použít 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 bodového/shromáždění můžete chtít počkat na všechny úkoly v sadě, pokud některý z nich není chybný, v takovém případě chcete přestat čekat, jakmile dojde k výjimce. Toho 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 => t.Result).ToArray());
    };

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

Vytváření datových struktur založených na úlohách

Kromě možnosti vytváření vlastních kombinátorů založených na úlohách má datovou strukturu a TaskTask<TResult> představuje jak výsledky asynchronní operace, tak i potřebnou synchronizaci pro spojení s ní, je výkonným typem, na kterém se vytvářejí vlastní datové struktury, které se mají použít v asynchronních scénářích.

AsyncCache

Jedním z důležitých aspektů úkolu je, že může být předán více příjemcům, všichni z nich mohou očekávat, zaregistrovat pokračování, získat jeho výsledek nebo výjimky (v případě Task<TResult>) atd. To dělá Task a Task<TResult> dokonale vhodné pro použití v asynchronní infrastruktuře 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>
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

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

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

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>.< Všechny dříve přístupné hodnoty z mezipaměti jsou uloženy v interním slovníku a AsyncCache zajistí, že se pro každý klíč vygeneruje jenom jeden úkol, a to i v případě, že je mezipaměť přístupná 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.

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

AsyncProducerConsumerCollection

Ú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. V případě modelu producenta/spotřebitele potřebujete k uložení práce vytvořené producenty vždy 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 producenti 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;
            }
        }
    }
}

S touto datovou strukturou můžete napsat například následující kód:

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

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:

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

Poznámka:

Obor System.Threading.Tasks.Dataflow názvů je k dispozici jako balíček NuGet. Pokud chcete nainstalovat sestavení, které obsahuje System.Threading.Tasks.Dataflow obor názvů, otevřete projekt v sadě Visual Studio, v nabídce Projekt zvolte Spravovat balíčky NuGet a vyhledejte System.Threading.Tasks.Dataflow balíček online.

Viz také