Megosztás a következőn keresztül:


A tevékenységalapú aszinkron minta felhasználása

Ha a tevékenységalapú aszinkron mintát (TAP) használja az aszinkron műveletekhez, visszahívásokkal blokkolás nélkül is elérheti a várakozást. A feladatok esetében ez olyan módszerekkel érhető el, mint a Task.ContinueWith. A nyelvi alapú aszinkron támogatás elrejti a visszahívásokat azáltal, hogy lehetővé teszi az aszinkron műveletek várakozását a normál vezérlési folyamaton belül, a fordító által létrehozott kód pedig ugyanezt az API-szintű támogatást biztosítja.

Végrehajtás felfüggesztése várakozással

A várt kulcsszót a C#-ban és a Várás operátort használhatja a Visual Basicben aszinkron várakozáshoz Task és Task<TResult> objektumokhoz. Amikor egy kifejezésre Taskvár, a await kifejezés típusa .void Amikor egy kifejezésre Task<TResult>vár, a await kifejezés típusa .TResult Egy await kifejezésnek egy aszinkron metódus törzsében kell történnie. (Ezeket a nyelvi funkciókat a .NET-keretrendszer 4.5-ben vezettük be.)

A fedőlap alatt a várva várt funkció folytatással visszahívást telepít a feladatra. Ez a visszahívás a felfüggesztés helyén folytatja az aszinkron metódust. Az aszinkron metódus folytatásakor, ha a várt művelet sikeresen befejeződött, és sikeres volt Task<TResult>, a rendszer visszaadja az aszinkron metódust TResult . Ha a Task várt vagy Task<TResult> várt állapot véget Canceled ért, a rendszer kivételt OperationCanceledException jelez. Ha a Task várt vagy Task<TResult> várt állapot véget Faulted ért, a hibát okozó kivétel ki lesz dobva. A Task hiba több kivétel miatt is hibás lehet, de a rendszer csak az egyik kivételt propagálja. A tulajdonság azonban kivételt AggregateException ad vissza, Task.Exception amely az összes hibát tartalmazza.

Ha egy szinkronizálási környezet (SynchronizationContext objektum) van társítva ahhoz a szálhoz, amely a felfüggesztéskor az aszinkron metódust futtatta (például ha a SynchronizationContext.Current tulajdonság nem null), az aszinkron metódus ugyanazon a szinkronizálási környezetben folytatódik a környezet metódusának Post használatával. Ellenkező esetben a felfüggesztéskor aktuális feladatütemezőre (TaskScheduler objektumra) támaszkodik. Ez általában az alapértelmezett feladatütemező (TaskScheduler.Default), amely a szálkészletet célozza. Ez a feladatütemező határozza meg, hogy a várt aszinkron művelet folytatódjon-e ott, ahol befejeződött, vagy az újrakezdés ütemezve legyen. Az alapértelmezett ütemező általában lehetővé teszi a folytatás futtatását azon a szálon, amelyen a várt művelet befejeződött.

Az aszinkron metódus meghívásakor szinkron módon hajtja végre a függvény törzsét, amíg az első várt kifejezés nem fejeződik be egy várva várt példányon, amely még nem fejeződött be, ekkor a hívás visszakerül a hívóhoz. Ha az aszinkron metódus nem ad vissza void, a függvény egy Task vagy Task<TResult> objektumot ad vissza a folyamatban lévő számításnak. Érvénytelen aszinkron metódusban, ha egy visszatérési utasítást észlel, vagy a metódus törzsének vége el van érve, a feladat végleges állapotban RanToCompletion fejeződik be. Ha egy kezeletlen kivétel miatt a vezérlés elhagyja az aszinkron metódus törzsét, a tevékenység az Faulted állapotban fejeződik be. Ha ez a kivétel egy OperationCanceledException, a tevékenység ehelyett az Canceled állapotban fejeződik be. Ily módon az eredmény vagy a kivétel végül közzé lesz téve.

Ennek a viselkedésnek számos fontos változata létezik. Teljesítménybeli okokból, ha egy tevékenység már befejeződött a tevékenység várt idejére, a rendszer nem engedélyezi a vezérlést, és a függvény továbbra is fut. Emellett az eredeti környezetbe való visszatérés nem mindig a kívánt viselkedés, és módosítható; ezt részletesebben a következő szakaszban ismertetjük.

Felfüggesztés és újrakezdés konfigurálása a Hozam és a ConfigureAwait használatával

Több módszer is nagyobb ellenőrzést biztosít az aszinkron metódusok végrehajtása felett. A metódussal Task.Yield például hozampontot vezethet be az aszinkron metódusba:

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

Ez egyenértékű az aszinkron közzétételsel vagy az aktuális környezetbe való visszaütemezéssel.

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

A módszert a Task.ConfigureAwait felfüggesztés és az újrakezdés jobb szabályozására is használhatja az aszinkron metódusban. Ahogy korábban említettük, a rendszer alapértelmezés szerint az aktuális környezetet rögzíti az aszinkron metódus felfüggesztésekor, és a rögzített környezetet használja az aszinkron metódus folytatásának újbóli folytatásának meghívásához. Sok esetben ez a kívánt viselkedés. Más esetekben előfordulhat, hogy nem érdekli a folytatási környezet, és jobb teljesítményt érhet el, ha elkerüli az ilyen bejegyzések vissza az eredeti kontextusba. Ennek engedélyezéséhez a metódussal Task.ConfigureAwait tájékoztassa a várva várt műveletet, hogy ne rögzítse és folytassa a környezetet, hanem folytassa a végrehajtást, ahol a várt aszinkron művelet befejeződött:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Aszinkron művelet megszakítása

A .NET-keretrendszer 4-től kezdődően a lemondást támogató TAP-metódusok legalább egy olyan túlterhelést biztosítanak, amely elfogadja a lemondási jogkivonatot (CancellationTokenobjektumot).

A lemondási jogkivonat egy lemondási jogkivonat forrásán (CancellationTokenSource objektumán) keresztül jön létre. A forrás tulajdonsága visszaadja Token a lemondási jogkivonatot, amely a forrás metódusának Cancel meghívásakor lesz jelezve. Ha például egy weblapot szeretne letölteni, és meg szeretné szakítani a műveletet, hozzon létre egy CancellationTokenSource objektumot, adja át a jogkivonatát a TAP metódusnak, majd hívja meg a forrás metódusát Cancel , amikor készen áll a művelet megszakítására:

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

Több aszinkron hívás megszakításához ugyanazt a jogkivonatot továbbíthatja az összes meghívásnak:

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

Azt is megteheti, hogy ugyanazt a jogkivonatot a műveletek egy szelektív részhalmazának adja át:

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

Fontos

A lemondási kérelmek bármely szálból kezdeményezhetők.

Az értéket bármely olyan metódusnak átadhatja CancellationToken.None , amely elfogad egy lemondási jogkivonatot, és jelzi, hogy a lemondás soha nem lesz kérve. Ez a CancellationToken.CanBeCanceled tulajdonság visszatérését falseokozza, és a hívott metódus ennek megfelelően optimalizálható. Tesztelési célokra egy előre lemondott lemondási jogkivonatot is átadhat, amelyet a logikai értéket elfogadó konstruktor használatával példányosít a rendszer, amely jelzi, hogy a jogkivonatnak már lemondott vagy nem visszavonható állapotban kell-e kezdődnie.

Ennek a lemondási módszernek számos előnye van:

  • Ugyanazt a lemondási jogkivonatot tetszőleges számú aszinkron és szinkron műveletnek átadhatja.

  • Ugyanannak a lemondási kérésnek a száma tetszőleges számú figyelő számára is elszaporodhat.

  • Az aszinkron API fejlesztője teljes mértékben ellenőrzi, hogy kérhető-e lemondás, és mikor lép érvénybe.

  • Az API-t használó kód szelektíven határozhatja meg azokat az aszinkron hívásokat, amelyekbe a lemondási kérelmek propagálása megtörténik.

Monitorozási folyamat

Egyes aszinkron metódusok az aszinkron metódusnak átadott folyamatillesztőn keresztül teszik elérhetővé a folyamatot. Vegyük például azt a függvényt, amely aszinkron módon letölt egy szöveges sztringet, és közben olyan előrehaladási frissítéseket hoz létre, amelyek tartalmazzák az eddig befejezett letöltés százalékos arányát. Egy ilyen módszer az alábbiak szerint használható egy Windows megjelenítési alaprendszer (WPF) alkalmazásban:

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

A beépített feladatalapú kombinátorok használata

A System.Threading.Tasks névtér számos metódust tartalmaz a feladatok írásához és használatához.

Task.Run

Az Task osztály számos Run metódust tartalmaz, amelyekkel egyszerűen ki lehet kapcsolni a munkát a Task szálkészletből vagy Task<TResult> a szálkészletből, például:

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

Ezen Run módszerek némelyike, például a Task.Run(Func<Task>) túlterhelés, rövidítésként létezik a TaskFactory.StartNew módszerhez. Ez a túlterhelés lehetővé teszi a várakozások használatát a kiszervezett munkán belül, például:

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

Az ilyen túlterhelések logikailag egyenértékűek a TaskFactory.StartNew metódus és a Unwrap párhuzamos feladattár bővítménymetódusának használatával.

Task.FromResult

Használja a FromResult metódust olyan helyzetekben, ahol az adatok már elérhetők lehetnek, és csak egy feladatvisszaküldött metódusból kell visszaadni a Task<TResult>következőt:

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 A metódus használatával aszinkron módon várakozhat több aszinkron műveletre, amelyek feladatként jelennek meg. A metódus több túlterheléssel rendelkezik, amelyek nem általános tevékenységek készletét vagy általános tevékenységek nem egységes készletét támogatják (például aszinkron módon több üres visszatérési műveletre várnak, vagy aszinkron módon várnak több értékvisszaadó metódusra, ahol az egyes értékek eltérő típusúak lehetnek), és támogatják az általános tevékenységek egységes készletét (például aszinkron módon várva több TResult-returning metódusra).

Tegyük fel, hogy több ügyfélnek szeretne e-maileket küldeni. Átfedheti az üzenetek küldését, így nem vár az egyik üzenet befejezésére a következő elküldése előtt. Azt is megtudhatja, hogy mikor fejeződtek be a küldési műveletek, és hogy történt-e hiba:

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

Ez a kód nem kezeli explicit módon az esetlegesen előforduló kivételeket, de lehetővé teszi a kivételek propagálását az await eredményül kapott tevékenységből WhenAll. A kivételek kezeléséhez használhatja a következőhöz hasonló kódot:

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

Ebben az esetben, ha egy aszinkron művelet meghiúsul, a rendszer az összes kivételt egy AggregateException kivételben összesítve fogja tárolni, amelyet a rendszer a TaskWhenAll metódus által visszaadott helyen tárol. A kulcsszó azonban csak az egyik kivételt await propagálja. Ha meg szeretné vizsgálni az összes kivételt, az alábbi módon írhatja át az előző kódot:

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

Vegyünk egy példát arra, hogy több fájlt töltünk le a webről aszinkron módon. Ebben az esetben az összes aszinkron művelet homogén eredménytípusokkal rendelkezik, és az eredmények könnyen elérhetők:

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

Ugyanazokat a kivételkezelési technikákat használhatja, mint az előző üres visszatérési forgatókönyvben:

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

A metódussal WhenAny aszinkron módon megvárhatja, amíg a feladatokként megadott több aszinkron művelet közül csak egy befejeződik. Ez a módszer négy elsődleges használati esetet szolgál ki:

  • Redundancia: Egy művelet többszöri végrehajtása és az első befejező kiválasztása (például több tőzsdei árfolyam-webszolgáltatással való kapcsolatfelvétel, amely egyetlen eredményt hoz létre, és kiválasztja azt, amelyik a leggyorsabb eredményt adja).

  • Interleaving: Több művelet indítása és várakozás az összes művelet befejezésére, de a feldolgozásuk során.

  • Szabályozás: Lehetővé teszi, hogy további műveletek kezdődjenek, miközben mások befejeződnek. Ez az összekapcsolási forgatókönyv kiterjesztése.

  • Korai mentés: A t1 tevékenység által képviselt művelet például csoportosítható egy WhenAny másik t2 tevékenységgel rendelkező tevékenységbe, és várakozhat a WhenAny tevékenységre. A t2 tevékenység időtúllépést, lemondást vagy egyéb jelzést jelenthet, amely miatt a WhenAny tevékenység a t1 befejezése előtt befejeződik.

Redundancia

Fontolja meg azt az esetet, amikor szeretne döntést hozni arról, hogy vásárol-e részvényt. Számos olyan részvényajánlási webszolgáltatás van, amelyben megbízik, de a napi terheléstől függően az egyes szolgáltatások különböző időpontokban lassúak lehetnek. A metódussal WhenAny értesítést kaphat, ha bármilyen művelet befejeződik:

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

Ellentétben WhenAlla sikeresen befejezett összes tevékenység felülíratlan eredményeivel, WhenAny a befejezett feladatot adja vissza. Ha egy tevékenység meghiúsul, fontos tudni, hogy a feladat meghiúsult, és ha egy tevékenység sikeres, fontos tudni, hogy melyik tevékenységhez van társítva a visszatérési érték. Ezért hozzá kell férnie a visszaadott tevékenység eredményéhez, vagy tovább kell várnia, ahogy ez a példa is mutatja.

A kivételeket ugyanúgy el kell tudnia fogadni, mint a kivételeket WhenAll. Mivel visszakapja a befejezett feladatot, várhatja, hogy a visszaadott tevékenység hibákat propagáljon, és try/catch megfelelően, például:

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

Emellett még akkor is, ha egy első tevékenység sikeresen befejeződik, a későbbi tevékenységek meghiúsulhatnak. Ezen a ponton több lehetősége is van a kivételek kezelésére: Megvárhatja, amíg az összes elindított tevékenység befejeződik, ebben az esetben használhatja a WhenAll módszert, vagy eldöntheti, hogy minden kivétel fontos, és naplózni kell. Ehhez a folytatásokkal értesítést kaphat, ha a tevékenységek aszinkron módon fejeződnek be:

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

vagy:

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

vagy akár:

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

Végül érdemes lehet megszakítani az összes többi műveletet:

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

Kihagyásos

Fontolja meg azt az esetet, amikor képeket tölt le az internetről, és feldolgoz minden képet (például hozzáadja a képet egy felhasználói felület vezérlőjéhez). A képeket egymás után dolgozza fel a felhasználói felületi szálon, de a lehető leggyorsabban szeretné letölteni a képeket. Azt sem szeretné, hogy a rendszerképeket mindaddig hozzáadja a felhasználói felülethez, amíg le nem tölti őket. Ehelyett a befejezésükkor szeretné hozzáadni őket.

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

Interleavinget is alkalmazhat olyan forgatókönyvekre, amelyek számításilag intenzív feldolgozást igényelnek a ThreadPool letöltött képeken, például:

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

Szabályozás

Vegyük az interleaving példát, azzal a kivételével, hogy a felhasználó annyi képet tölt le, hogy a letöltéseket szabályozni kell; Például azt szeretné, hogy csak bizonyos számú letöltés történjen egyidejűleg. Ennek eléréséhez elindíthatja az aszinkron műveletek egy részhalmazát. A műveletek befejezésével további műveleteket indíthat el a helyükbe:

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

Korai mentőcsomag

Fontolja meg, hogy aszinkron módon várakozik egy művelet befejezésére, miközben egyidejűleg válaszol egy felhasználó lemondási kérelmére (például a felhasználó a mégse gombra kattintott). Az alábbi kód a következő forgatókönyvet mutatja be:

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

Ez az implementáció újra engedélyezi a felhasználói felületet, amint úgy dönt, hogy kisegíti, de nem szakítja meg az alapul szolgáló aszinkron műveleteket. Egy másik lehetőség a függőben lévő műveletek lemondása, amikor úgy dönt, hogy kisegíti a műveletet, de a felhasználói felületet nem kell újra létrehoznia a műveletek befejezéséig, ami a lemondási kérelem miatt korai befejezést jelenthet:

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

A korai mentőcsomag egy másik példája a WhenAny módszer és a Delay metódus együttes használata, amint azt a következő szakaszban tárgyaljuk.

Task.Delay

A metódussal Task.Delay szüneteltetéseket vezethet be egy aszinkron metódus végrehajtásába. Ez számos funkcióhoz hasznos, beleértve a lekérdezési ciklusok kiépítését és a felhasználói bemenetek előre meghatározott ideig történő kezelését. A Task.Delay módszer a várakozási időkorlátok implementálásával Task.WhenAny együtt is hasznos lehet.

Ha egy nagyobb aszinkron művelet részét képező tevékenység (például egy ASP.NET webszolgáltatás) túl sokáig tart, az általános művelet szenvedhet, különösen akkor, ha a művelet nem fejeződik be. Ezért fontos, hogy időtúllépést lehessen elérni, amikor aszinkron műveletre vár. A szinkron Task.Waités Task.WaitAnyTask.WaitAlla metódusok időtúllépési értékeket fogadnak el, de a megfelelő/TaskFactory.ContinueWhenAllTaskFactory.ContinueWhenAny és a korábban említett Task.WhenAll/Task.WhenAny metódusok nem. Ehelyett időtúllépést alkalmazhat és Task.WhenAny kombinálhat Task.Delay is.

Tegyük fel például, hogy a felhasználói felületi alkalmazásban le szeretne tölteni egy képet, és le szeretné tiltani a felhasználói felületet a rendszerkép letöltése közben. Ha azonban a letöltés túl sokáig tart, újra engedélyeznie kell a felhasználói felületet, és el kell vetnie a letöltést:

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

Ugyanez vonatkozik több letöltésre is, mert WhenAll egy feladatot ad vissza:

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

Feladatalapú kombinátorok létrehozása

Mivel egy tevékenység képes teljes mértékben képviselni az aszinkron műveletet, és szinkron és aszinkron képességeket biztosít a művelethez való csatlakozáshoz, az eredmények lekéréséhez és így tovább, hasznos kombinátorokból álló kódtárakat hozhat létre, amelyek feladatokat alkotnak a nagyobb minták létrehozásához. Az előző szakaszban leírtak szerint a .NET számos beépített kombinátort tartalmaz, de sajátot is létrehozhat. A következő szakaszok számos példát mutatnak be a lehetséges kombinátor módszerekre és típusokra.

RetryOnFault

Sok esetben érdemes lehet újrapróbálkozhat egy művelettel, ha egy korábbi kísérlet meghiúsul. Szinkron kód esetén létrehozhat egy segédmetódust, például RetryOnFault az alábbi példában ehhez:

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

Szinte azonos segédmetódust hozhat létre a TAP használatával implementált aszinkron műveletekhez, és így feladatokat adhat vissza:

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

Ezután ezt a kombinátort használhatja az újrapróbálkozések az alkalmazás logikájába való kódolásához; például:

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

A függvényt tovább bővítheti RetryOnFault . A függvény például elfogadhat egy másikat Func<Task> is, amelyet az újrapróbálkozási műveletek között hív meg, hogy megállapítsa, mikor próbálja újra a műveletet; például:

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

Ezután a függvényt az alábbiak szerint használhatja, hogy várjon egy másodpercet a művelet újrapróbálkozása előtt:

// 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éha kihasználhatja a redundancia előnyeit a művelet késésének és a sikeresség esélyének javítása érdekében. Fontolja meg több olyan webszolgáltatást, amely tőzsdei árfolyamokat biztosít, de a nap különböző szakaszaiban minden szolgáltatás különböző minőségi és válaszidőket biztosíthat. Az ingadozások kezelése érdekében kéréseket adhat ki az összes webszolgáltatásnak, és amint választ kap egytől, a fennmaradó kéréseket is visszavonhatja. Implementálhat egy segédfüggvényt, hogy könnyebben implementálhassa ezt a gyakori mintát, amely több műveletet indít el, várakozik bármelyikre, majd megszakítja a többit. A NeedOnlyOne következő példában szereplő függvény ezt a forgatókönyvet szemlélteti:

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

Ezt a függvényt a következőképpen használhatja:

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

Interleaved Operations

Előfordulhat, hogy teljesítményproblémák merülnek fel azzal kapcsolatban, hogy a WhenAny metódussal támogat egy összefüggő forgatókönyvet, amikor nagy feladatkészletekkel dolgozik. Minden hívás azt eredményezi, hogy WhenAny az egyes tevékenységek regisztrálva lesznek a folytatásban. Az N számú tevékenység esetében ez O(N2) folytatásokat eredményez, amelyeket az összekötő művelet élettartama során hoztak létre. Ha nagy feladatkészlettel dolgozik, egy kombinátor használatával (Interleaved az alábbi példában) kezelheti a teljesítményproblémát:

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

Ezután a kombinátor használatával feldolgozhatja a feladatok eredményeit, miközben befejeződnek; például:

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

WhenAllOrFirstException

Bizonyos pont-/gyűjtési forgatókönyvekben érdemes lehet megvárni egy készlet összes feladatát, kivéve, ha az egyik hibás, ebben az esetben a kivétel bekövetkezésekor azonnal le szeretné állítani a várakozást. Ezt egy kombinátor metódussal teheti meg, például WhenAllOrFirstException az alábbi példában:

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

Feladatalapú adatstruktúrák létrehozása

Amellett, hogy egyéni feladatalapú kombinátorok hozhatók létre, olyan adatstruktúrával TaskTask<TResult> rendelkezik, amely egy aszinkron művelet eredményeit és a hozzá való csatlakozáshoz szükséges szinkronizálást is képviseli, hatékony típussá teszi, amelyre az aszinkron forgatókönyvekben használandó egyéni adatstruktúrák hozhatók létre.

AsyncCache

A feladat egyik fontos eleme, hogy több fogyasztónak is átadható, akik mindannyian várják, regisztrálják vele a folytatásokat, megkapják annak eredményét vagy kivételeit (az eset esetén Task<TResult>), és így tovább. Ez lehetővé teszi Task és Task<TResult> tökéletesen alkalmas az aszinkron gyorsítótárazási infrastruktúrában való használatra. Íme egy példa egy kis, de nagy teljesítményű aszinkron gyorsítótárra, amely Task<TResult>a következőkre épül:

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

Az AsyncCache<TKey,TValue> osztály a konstruktorának delegáltként fogad el egy függvényt, amely egy függvényt TKey vesz fel, és visszaad egy Task<TResult>. A gyorsítótárból korábban elért értékek a belső szótárban vannak tárolva, és biztosítja AsyncCache , hogy kulcsonként csak egy tevékenység legyen létrehozva, még akkor is, ha a gyorsítótár egyidejűleg érhető el.

Létrehozhat például egy gyorsítótárat a letöltött weblapokhoz:

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

Ezt a gyorsítótárat ezután aszinkron metódusokban is használhatja, amikor szüksége van egy weblap tartalmára. Az AsyncCache osztály biztosítja, hogy a lehető legkevesebb oldalt töltse le, és gyorsítótárazza az eredményeket.

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

A feladatok segítségével adatstruktúrákat is létrehozhat az aszinkron tevékenységek koordinálásához. Fontolja meg a klasszikus párhuzamos tervezési minták egyikét: gyártó/fogyasztó. Ebben a mintában a termelők olyan adatokat hoznak létre, amelyeket a fogyasztók használnak fel, és a termelők és a fogyasztók párhuzamosan futhatnak. A fogyasztó például feldolgozza az 1. elemet, amelyet korábban egy olyan gyártó hozott létre, aki most a 2. elemet állítja elő. A gyártói/fogyasztói minta esetében mindig szükség van valamilyen adatstruktúrára a termelők által létrehozott munka tárolásához, hogy a felhasználók értesülhessenek az új adatokról, és megtalálják azokat, ha elérhetők.

Íme egy egyszerű, feladatokra épülő adatstruktúra, amely lehetővé teszi az aszinkron metódusok gyártóként és fogyasztóként való használatát:

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

Ezzel az adatstruktúrával olyan kódot írhat, mint a következő:

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

A System.Threading.Tasks.Dataflow névtér tartalmazza a BufferBlock<T> típust, amelyet hasonló módon használhat, de egyéni gyűjteménytípus létrehozása nélkül:

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

Feljegyzés

A System.Threading.Tasks.Dataflow névtér NuGet-csomagként érhető el. A névteret tartalmazó System.Threading.Tasks.Dataflow szerelvény telepítéséhez nyissa meg a projektet a Visual Studióban, válassza a NuGet-csomagok kezelése lehetőséget a Project menüben, és keressen rá online a System.Threading.Tasks.Dataflow csomagra.

Lásd még