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

Pokud pro práci s asynchronními operacemi používáte asynchronní vzor založený 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 je Task.ContinueWith . Asynchronní podpora založená na jazyce skrývá 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í provádění pomocí await

Můžete použít klíčové slovo await v jazyce C# a operátor Await v Visual Basic asynchronně await Task a Task<TResult> objekty. Když čekáte na Task , je await výraz typu void . Když čekáte na Task<TResult> , je await výraz typu TResult . Výraz await se musí vyskytovat uvnitř těla asynchronní metody. (Tyto jazykové funkce byly představeny ve .NET Framework 4.5.)

Funkce await na úlohu na úlohu nainstaluje zpětné volání pomocí pokračování. Toto zpětné volání obnoví asynchronní metodu v okamžiku pozastavení. Když je asynchronní metoda obnovena a očekávaná operace se úspěšně dokončila a byla Task<TResult> , vrátí se její metoda TResult . Pokud byl Task objekt nebo, který byl očekáván, ukončen ve stavu Task<TResult> , je Canceled OperationCanceledException vyvolána výjimka. Pokud byl Task objekt nebo, který byl očekáván, ukončen ve stavu , je vyvolána výjimka, která Task<TResult> Faulted způsobila chybu. Chyba může být výsledkem více výjimek, ale rozšíří se pouze jedna Task z těchto výjimek. Vlastnost však Task.Exception vrátí AggregateException výjimku, která obsahuje všechny chyby.

Je-li kontext synchronizace ( objekt) přidružen k vláknu, které v době pozastavení bylo spuštěno asynchronní metodu (například pokud vlastnost není ), asynchronní metoda pokračuje ve stejném kontextu synchronizace pomocí metody SynchronizationContext SynchronizationContext.Current null Post kontextu. V opačném případě spoléhá na plánovač úkolů ( objekt), který byl aktuální v TaskScheduler době pozastavení. Obvykle se jedná o výchozí plánovač úkolů ( 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 jestli má být naplánované obnovení. Výchozí plánovač obvykle umožňuje pokračování běžet ve vlákně, které se dokončila očekávaná operace.

Při volání asynchronní metody synchronně provede tělo funkce až do prvního výrazu await na instanci await, která ještě není dokončena, a v tomto okamžiku se volání vrátí volajícímu. Pokud asynchronní metoda nevrátí , objekt nebo je vrácen k reprezentaci void Task Task<TResult> probíhajícího výpočtu. Pokud je v asynchronní metodě bez void zjištěn příkaz return 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 tato výjimka OperationCanceledException , úloha místo toho skončí ve stavu Canceled . Tímto způsobem je výsledek nebo výjimka nakonec publikována.

Toto chování má několik důležitých variant. Z důvodů výkonu, pokud se úkol již dokončil v době, kdy je úkol očekáván, ovládací prvek není proveden a funkce se stále spouští. Kromě toho návrat k původnímu kontextu není vždy požadovaným chováním a lze ho změnit. To je podrobněji popsáno v další části.

Konfigurace pozastavení a opětovného pozastavení s výnosem a konfigurací

Několik metod poskytuje větší kontrolu nad prováděním asynchronní metody. Metodu můžete například použít Task.Yield k zavedení bodu výnosu do asynchronní metody:

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

To je ekvivalentem asynchronního publiková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 pro lepší kontrolu nad pozastavením a znovunačtení v Task.ConfigureAwait asynchronní metodě. Jak už bylo zmíněno dříve, ve výchozím nastavení je aktuální kontext zachycen v době pozastavení asynchronní metody a tento zachycený kontext se používá k vyvolání pokračování asynchronní metody při opětovném spuštění. V mnoha případech se jedná o přesné chování, které chcete. V jiných případech vám nemusí na kontextu pokračování záleží a můžete dosáhnout lepšího výkonu tím, že se takovým příspěvkům vyhnete zpět do původního kontextu. Pokud to chcete povolit, použijte metodu k informování operace await, aby se nezachytávání a obnovování v kontextu, ale aby bylo možné pokračovat v provádění všude tam, kde byla dokončena asynchronní operace, která byla Task.ConfigureAwait očekávána:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Zrušení asynchronní operace

Počínaje .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 objekt). Vlastnost zdroje vrátí token zrušení, který bude signalizován při volání Token Cancel metody zdroje. Pokud například chcete stáhnout jednu webovou stránku a chcete mít možnost operaci zrušit, vytvoříte objekt, předáte jeho token metodě TAP a až budete připraveni operaci zrušit, zavoláte metodu CancellationTokenSource Cancel zdroje:

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 volání, můžete všem voláním předat stejný token:

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 předat stejný token 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();

Žádosti o zrušení je možné iniciovat z libovolného vlákna.

Hodnotu můžete předat jakékoli metodě, která přijímá token zrušení, a označit tak, že zrušení CancellationToken.None nebude nikdy požadováno. To CancellationToken.CanBeCanceled způsobí, že vlastnost vrátí false a volána 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ří pomocí konstruktoru, který přijímá logickou hodnotu, která označuje, jestli má být token začíná ve stavu zrušeno nebo není zrušitelné.

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ý požadavek na zrušení může být naslouchacích požadavkům procháněn do libovolného počtu naslouchacích.

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

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

Sledování průběhu

Některé asynchronní metody zpřístupňuje průběh prostřednictvím rozhraní průběhu předaného asynchronní metodě. Představte si například funkci, která asynchronně stahuje textový řetězec a současně vyvolává aktualizace průběhu, které zahrnují procento dosud dokončeného stahování. Tuto metodu lze použít 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ě

Obor System.Threading.Tasks názvů obsahuje několik metod pro sestavení a práci s úkoly.

Task.Run

Třída obsahuje několik metod, které vám umožňují snadno přenačtení práce jako nebo do fondu Task Run Task Task<TResult> 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 TaskFactory.StartNew metodu . Jiná přetížení, například , umožňují použít funkci await v rámci práce se Task.Run(Func<Task>) snižováním zátěže, 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í k použití metody ve spojení s rozšiřující metodou TaskFactory.StartNew Unwrap v Task Parallel Library.

Task.FromResult

Tuto metodu použijte ve scénářích, kde už data mohou být dostupná a je potřeba je vrátit z metody vracející úlohy, která FromResult se převedou do Task<TResult> třídy :

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

Pomocí metody WhenAll 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 obecných úloh nebo nejednotnou sadu obecných úloh (například asynchronní čekání na více operací vracejících identifikátory nebo asynchronní čekání na více metod vracejících hodnotu, kde každá hodnota může mít jiný typ) a podporovat jednotnou sadu obecných úloh (například asynchronní čekání na více metod vracejících TResult hodnotu).

Řekněme, že chcete posílat e-mailové zprávy několika zákazníkům. Odesílání zpráv se může překrývat, takže nečekáte na dokončení jedné zprávy před odesláním další zprávy. Můžete také zjistit, kdy se operace odesílání dokončily a jestli došlo k chybám:

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

Tento kód explicitně nezovládá výjimky, ke kterým může dojít, ale umožňuje šíření výjimek mimo v await výsledném úkolu z WhenAll . Ke 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)
{
    ...
}

Pokud v tomto případě selže nějaká asynchronní operace, všechny výjimky budou konsolidovány ve výjimce, která je uložena v metodě vrácené AggregateException Task metodou WhenAll . Klíčové slovo ale rozšíří pouze jednu z těchto await 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 je snadné získat přístup k výsledkům:

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

Můžete použít stejné techniky zpracování výjimek, které jsme probírali v předchozím scénáři vracející se výjimky:

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 můžete použít k asynchronnímu čekání pouze na jednu z několika asynchronních operací WhenAny reprezentovaných jako úkoly k dokončení. Tato metoda slouží ke čtyřem hlavním případům použití:

  • Redundance: Provedení vícenásobné operace a výběr operace, která se dokončí jako první (například kontaktování více webových služeb nabídky akcií, které vytvoří jeden výsledek, a výběrem té, která se dokončí nejrychleji).

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

  • Omezování: Umožňuje, aby další operace začaly s tím, jak se ostatní dokončují. Toto je rozšíření scénáře prokládaného.

  • Časná operace: Například operace reprezentovaná úlohou t1 může být seskupena v úkolu s jiným úkolem t2 a můžete na úkol WhenAny WhenAny počkat. Úloha t2 může představovat časový limit, zrušení nebo nějaký jiný signál, který způsobí dokončení úlohy WhenAny před dokončením t1.

Redundance

Zamyslete se nad případem, kdy se chcete rozhodnout, jestli koupit akcie. Existuje několik webových služeb s doporučeními akcií, které důvěřujete, ale v závislosti na denním zatížení může být každá služba v různou dobu pomalá. Pokud chcete WhenAny dostávat oznámení po dokončení jakékoli operace, můžete použít metodu :

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 , který vrací nezabalené výsledky všech úspěšně dokončených úkolů, vrátí WhenAll WhenAny dokončený úkol. Pokud úloha selže, je důležité vědět, že selhala, a pokud je úkol úspěšný, je důležité vědět, ke kterému úkolu je vrácená hodnota přidružena. Proto potřebujete získat přístup k výsledku vráceného úkolu nebo ho očekávat více, jak ukazuje tento příklad.

Stejně jako v WhenAll případě musíte být schopni vyhovět výjimkám. Vzhledem k tomu, že obdržíte dokončenou úlohu zpět, můžete očekávat, že vrácená úloha bude obsahovat chyby, 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);
    }
}

Kromě toho, i když se první úkol úspěšně dokončí, můžou následné úkoly selhat. V tomto okamžiku máte k dispozici několik možností pro práci s výjimkami: můžete počkat, až se všechny spuštěné úlohy dokončí, a v takovém případě můžete použít WhenAll metodu nebo se můžete rozhodnout, že jsou všechny výjimky důležité a musí být zaprotokolovány. K tomu můžete použít pokračování pro příjem oznámení, když se úkoly asynchronně dokončují:

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

Potřeba prokládání

Vezměte v úvahu případ, kdy stahujete obrázky z webu a zpracováváte jednotlivé Image (například přidáním obrázku do ovládacího prvku uživatelského rozhraní). Bitové kopie se zpracovávají postupně na vlákně uživatelského rozhraní, ale chcete image stahovat co nejaktuálnější, pokud je to možné. Nechcete také přidržet obrázky 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 přejezd na scénář, který zahrnuje výpočetně náročné zpracování na ThreadPool stažených obrázcích, 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{}
}

Throttling

Vezměte v úvahu příklad prokládání s tím rozdílem, že uživatel je stahuje, takže mnoho imagí, které je potřeba stáhnout, je nutné omezit. například chcete, aby bylo možné současně provést pouze určitý počet souborů ke stažení. K tomuto účelu můžete spustit podmnožinu asynchronních operací. Po dokončení operací můžete spustit další operace, které zabírají místo:

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é Bailout

Vezměte v úvahu, že budete čekat asynchronně, než se operace dokončí, a současně reagovat na požadavek na zrušení uživatele (například uživatel kliknul na tlačítko Storno). Tento scénář je znázorněný v následujícím kódu:

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í hned po rozhodnutí Bail, ale neruší základní asynchronní operace. Další možností je zrušit probíhající operace, když se rozhodnete Bail, ale nebudete moci znovu vytvořit uživatelské rozhraní, dokud se operace nedokončí, potenciálně v důsledku ukončení z důvodu žá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 Bailout je použití WhenAny metody ve spojení s Delay metodou, jak je popsáno v další části.

Task.Delay

Metodu lze použít Task.Delay k zavedení pozastavení do asynchronního zpracování metody. To je užitečné pro mnoho druhů funkcí, včetně vytváření smyček cyklického dotazování a zpoždění manipulace s uživatelským vstupem pro předem stanovenou dobu. Task.DelayMetoda může být také užitečná v kombinaci s Task.WhenAny pro implementaci časových limitů na await.

pokud úkol, který je součástí větší asynchronní operace (například ASP.NET webové služby), trvá příliš dlouho, může to být způsobeno tím, že by celková operace byla neúspěšná, zejména v případě, že se nepovede dřív. Z tohoto důvodu je důležité mít při čekání na asynchronní operaci časový limit. Synchronní Task.Wait metody, Task.WaitAll a Task.WaitAny akceptují hodnoty časového limitu, ale odpovídající TaskFactory.ContinueWhenAll / Task.WhenAny a výše uvedené Task.WhenAll / Task.WhenAny metody ne. Místo toho můžete použít Task.Delay a Task.WhenAny v kombinaci k implementaci časového limitu.

Například v aplikaci uživatelského rozhraní řekněme, že chcete stáhnout image a zakázat uživatelské rozhraní při stahování image. Pokud ale stahování trvá příliš dlouho, budete chtít znovu povolit uživatelské rozhraní a zahodit 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 souborů ke stažení, protože WhenAll vrátí ú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; }
}

Sestavování kombinátory založených na úlohách

Vzhledem k tomu, že úloha může zcela představovat asynchronní operaci a poskytovat synchronní a asynchronní možnosti pro připojení k operaci, načítání výsledků a tak dále, můžete vytvořit užitečné knihovny kombinátory, které vytvářejí úkoly pro vytváření větších vzorů. Jak je popsáno v předchozí části, rozhraní .NET obsahuje několik integrovaných kombinátory, ale můžete si také vytvořit vlastní. Následující části obsahují několik příkladů potenciálních metod a typů kombinátorem.

RetryOnFault

V mnoha situacích můžete zkusit operaci zopakovat, pokud se předchozí pokus nezdaří. V případě synchronního kódu můžete vytvořit pomocnou metodu, například RetryOnFault v následujícím příkladu, k provedení této akce:

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 klepnutím a následně vracet ú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);
}

Pak můžete použít tuto kombinátorem 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ál rozšířit RetryOnFault . Funkce například může přijmout další Func<Task> , který bude vyvolán mezi opakovanými pokusy, aby bylo možné určit, kdy se má operace opakovat. Pří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);
}

Potom můžete funkci použít následujícím způsobem, aby před opakováním operace čekala 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

V některých případech můžete využít redundanci a zlepšit latenci operace a šance na úspěch. Vezměte v úvahu více webových služeb, které poskytují nabídky, ale v různou dobu může každá služba poskytovat různé úrovně kvality a doby odezvy. Pokud chcete řešit tyto výkyvy, můžete vydávat požadavky na všechny webové služby a hned po obdržení odpovědi zrušit zbývající požadavky. Můžete implementovat pomocnou funkci, která usnadňuje implementaci tohoto běžného vzoru spouštění více operací, čekání na jakékoli a následné zrušení zbytku. NeedOnlyOneFunkce v následujícím příkladu znázorňuje tento scénář:

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ádané operace

V případě, že WhenAny pracujete s velkými sadami úkolů, můžete při použití metody pro podporu scénáře využít potenciální problémy s výkonem. Každé volání, které má WhenAny za následek pokračování zaregistrované u každého úkolu. Pro N počet úkolů to vede k pokračování v počtu (N2), které bylo vytvořeno během doby trvání prokládání operací. Pokud pracujete s velkou sadou úkolů, můžete k vyřešení problému s výkonem použít kombinátorem ( 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;
}

Pak můžete použít kombinátorem ke zpracování výsledků úloh po jejich dokončení. například:

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

WhenAllOrFirstException

V určitých bodových nebo sběrných scénářích můžete chtít počkat na všechny úlohy v sadě, pokud jedna z nich selže, a v takovém případě se chcete přestat čekat, jakmile dojde k výjimce. To lze provést pomocí metody kombinátorem, jako 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ářet vlastní kombinátory založenou na úlohách, které mají datovou strukturu Task a Task<TResult> které představují jak výsledky asynchronní operace, tak i nutná synchronizace pro připojení, díky tomu je účinný typ, na který se mají vytvářet 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ána více příjemcům, z nichž to může očekávat, zaregistrovat pokračování s ním, získat výsledek nebo výjimky (v případě Task<TResult> ) a tak dále. Tato technologie Task je Task<TResult> naprosto 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 postavené nad 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("loader");
        _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;
        }
    }
}

Třída AsyncCache <TKey,TValue> akceptuje jako delegáta funkci, která přijímá TKey a vrátí Task<TResult> . Všechny dříve použité hodnoty z mezipaměti se ukládají do interního slovníku a AsyncCache zajišťují, že se pro každý klíč generuje jenom jeden úkol, a to i v případě, že k mezipaměti dojde souběžně.

Můžete například sestavit 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 vždy, když potřebujete obsah webové stránky. AsyncCacheTřída zajišťuje, že budete stahovat 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

Úkoly můžete použít také k vytváření datových struktur pro koordinaci asynchronních aktivit. Vezměte v úvahu jeden z klasických paralelních vzorů návrhu: producent/příjemce. V tomto vzoru producenti generují data, která jsou využívána příjemci, a producenti a spotřebitelé můžou běžet paralelně. Například příjemce zpracuje položku 1, která byla dříve vygenerována výrobcem, který nyní vyrábí položku 2. Pro vzorek producent/příjemce invariably potřebovat určitou datovou strukturu pro ukládání práce vytvořené producenty, aby se příjemci mohli dostat k oznámením o nových datech a najít je, když jsou k dispozici.

Tady je jednoduchá datová struktura, která je postavená na úkolech, která umožňuje použití asynchronních metod jako výrobců 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;
            }
        }
    }
}

Pomocí této struktury dat můžete napsat kód, například následující:

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

System.Threading.Tasks.DataflowObor názvů zahrnuje BufferBlock<T> typ, který lze použít podobným způsobem, ale bez nutnosti sestavení vlastního typu 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

System.Threading.Tasks.Dataflowobor názvů je k dispozici jako balíček NuGet. chcete-li nainstalovat sestavení, které obsahuje System.Threading.Tasks.Dataflow obor názvů, otevřete projekt v Visual Studio, v nabídce Project vyberte možnost spravovat NuGet balíčky a vyhledejte System.Threading.Tasks.Dataflow balíček online.

Viz také