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 a minta olyan módszereket használ, 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 várakozik egy Task-re, a await kifejezés típusa void. Amikor vár egy Task<TResult>, a await kifejezés típusa TResult. Egy await kifejezésnek egy aszinkron metódus törzsében kell történnie. (Ezek a nyelvi funkciók a .NET Framework 4.5-ben lettek bevezetve.)

A háttérben az await funkció egy folytatás használatával 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 Task<TResult>, akkor a művelet TResult értéke kerül visszaadásra. Ha a Task vagy Task<TResult> várt objektum a Canceled állapottal ért véget, akkor OperationCanceledException kivételt dob. Ha a Task vagy Task<TResult> várt végállapot Faulted állapotban ért véget, a hibát okozó kivétel dobódik. A Task hiba több kivétel miatt is hibás lehet, de a rendszer csak az egyik kivételt propagálja. Azonban a Task.Exception tulajdonság egy kivételt AggregateException ad vissza, 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.

Amikor aszinkron metódust hív meg, az szinkron módon végrehajtja a függvény törzsét egészen addig, amíg egy még nem befejezett váró példányon az első várakozási kifejezés meg nem jelenik, ekkor a hívás visszakerül a hívóhoz. Ha az aszinkron metódus nem ad visszavoid, akkor egy Task vagy több Task<TResult> objektumot ad vissza, amely a folyamatban lévő számítást jelöli. Nem-void aszinkron metódusban, ha egy visszatérési utasítást talál, vagy a metódus törzsének végére ér, a feladat a 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 feladat már befejeződött amikorra a feladat végrehajtására várunk, a vezérlés nem adódik át, és a függvény tovább fut. Emellett az eredeti környezetbe való visszatérés nem mindig a kívánt viselkedés, és módosítható; ezt a viselkedést a következő szakaszban részletesebben ismertetjük.

Felfüggesztés és újrakezdés konfigurálása a yield é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 a módszer egyenértékű az aktuális környezethez való aszinkron közzétételsel vagy ütemezéssel.

public static async Task YieldLoopExample()
{
    await Task.Run(async delegate
    {
        for (int i = 0; i < 1000000; i++)
        {
            await Task.Yield(); // fork the continuation into a separate work item
        }
    });
}
Public Async Function YieldLoopExample() As Task
    Await Task.Run(Async Function()
                       For i As Integer = 0 To 999999
                           Await Task.Yield() ' fork the continuation into a separate work item
                       Next
                   End Function)
End Function

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 a visszatérést az eredeti környezethez. Annak érdekében, hogy engedélyezze ezt a viselkedést, használja az Task.ConfigureAwait metódust, hogy jelezze a várakozó műveletnek, ne rögzítse, és folytassa a környezet kontextusát, hanem folytassa a végrehajtást ott, ahol az aszinkron művelet befejeződött.

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Awaitables, ConfigureAwait és SynchronizationContext

await Bármilyen olyan típussal működik, amely megfelel a várt kifejezési mintának, nem csak Task. A típus akkor várakoztatható, ha egy kompatibilis GetAwaiter metódust biztosít, amely típust ad vissza a IsCompleted, OnCompleted és GetResult tagokkal. A legtöbb nyilvános API-ban adja vissza Task, Task<TResult>ValueTaskvagy ValueTask<TResult>. Egyéni várható elemeket csak speciális forgatókönyvekhez használhat.

Akkor használja a ConfigureAwait-t, ha a folytatáshoz nincs szükség a hívó környezetére. A felhasználói felületet frissítéseket tartalmazó alkalmazáskódban gyakran szükség van a környezet rögzítésére. Az újrafelhasználható könyvtári kódban ConfigureAwait(false)-t általában előnyben részesítik, mivel elkerüli a szükségtelen körülményezeti ugrásokat, és csökkenti a blokkolt hívók holtpont kockázatát.

ConfigureAwait(false) a folytatás ütemezését módosítja, nem ExecutionContext a folyamatot. A környezet viselkedésének részletesebb magyarázatát a ExecutionContext és a SynchronizationContext című témakörben talál.

Aszinkron művelet megszakítása

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

Lemondási jogkivonatot egy lemondási jogkivonat forrásán (CancellationTokenSource objektumán) keresztül hozhat létre. A forrás Token tulajdonsága visszaadja a visszavonási jogkivonatot, amely jelzi, amikor meghívják a forrás Cancel metódusát.

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

Ha például egyetlen 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();
    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

Bármely szál kezdeményezhet lemondási kéréseket.

Az értéket bármely olyan metódusnak átadhatja CancellationToken.None , amely elfogad egy lemondási jogkivonatot, hogy jelezze, hogy a lemondás soha nem kérhető. Ez az érték 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 tokent tetszőleges számú aszinkron és szinkron művelethez átadhatja.

  • Ugyanez a lemondási kérelem bármilyen számú figyelőhöz eljuthat.

  • Az aszinkron API fejlesztője teljes körűen ellenőrzi, hogy kérhető-e lemondás, és mikor lép érvénybe.

  • Az API-t használó kód szelektíven meghatározhatja, hogy mely aszinkron hívások fogadhatják a törlési kéréseket.

A folyamat monitorozása

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 metódust 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.

Megjegyzés:

Ebben a szakaszban számos kódminta Bitmap használ, amelyhez a System.Drawing.Common csomag szükséges, és csak Windows támogatott. Az általuk bemutatott aszinkron minták minden platformra érvényesek; nem Windows célokra helyettesítsen platformfüggetlen képalkotó könyvtárat.

Task.Run

Az Task osztály számos Run metódust tartalmaz, amelyekkel egyszerűen átadható a munka a szálkészletnek Task vagy Task<TResult> formájában. Például:

public static async Task TaskRunBasicExample()
{
    int answer = 42;
    string result = await Task.Run(() =>
    {
        // … do compute-bound work here
        return answer.ToString();
    });
    Console.WriteLine(result);
}
Public Async Function TaskRunBasicExample() As Task
    Dim answer As Integer = 42
    Dim result As String = Await Task.Run(Function()
                                              ' … do compute-bound work here
                                              Return answer.ToString()
                                          End Function)
    Console.WriteLine(result)
End Function

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, hogy a kiszervezett munkán belül használja a await elemet. Például:

public static async Task TaskRunAsyncExample()
{
    Bitmap image = await Task.Run(async () =>
    {
        using Bitmap bmp1 = await Stubs.DownloadFirstImageAsync();
        using Bitmap bmp2 = await Stubs.DownloadSecondImageAsync();
        return Stubs.Mashup(bmp1, bmp2);
    });
}
Public Async Function TaskRunAsyncExample() As Task
    Dim image As Bitmap = Await Task.Run(Async Function()
                                             Using bmp1 As Bitmap = Await Stubs.DownloadFirstImageAsync()
                                                 Using bmp2 As Bitmap = Await Stubs.DownloadSecondImageAsync()
                                                     Return Stubs.Mashup(bmp1, bmp2)
                                                 End Using
                                             End Using
                                         End Function)
End Function

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, és ezeket csak vissza kell adni egy feladatot visszaadó metódusból a Task<TResult> következőbe.

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

static async Task<int> GetValueAsyncInternal(string key)
{
    await Task.Delay(1);
    return 0;
}
Public Function GetValueAsync(key As String) As Task(Of Integer)
    Dim cachedValue As Integer
    If Stubs.TryGetCachedValue(cachedValue) Then
        Return Task.FromResult(cachedValue)
    Else
        Return GetValueAsyncInternal(key)
    End If
End Function

Private Async Function GetValueAsyncInternal(key As String) As Task(Of Integer)
    Await Task.Delay(1)
    Return 0
End Function

Task.WhenAll

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-mailt 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ődnek be a küldési műveletek, és hogy előfordulnak-e hibák:

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 feladatból, amely az WhenAll eredménye. A kivételek kezeléséhez használja a következőhöz hasonló kódot:

public static async Task WhenAllWithCatch()
{
    IEnumerable<Task> asyncOps = from addr in Stubs.addrs select Stubs.SendMailAsync(addr);
    try
    {
        await Task.WhenAll(asyncOps);
    }
    catch (Exception exc)
    {
        Console.WriteLine(exc);
    }
}
Public Async Function WhenAllWithCatch() As Task
    Dim asyncOps As IEnumerable(Of Task) = From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)
    Try
        Await Task.WhenAll(asyncOps)
    Catch exc As Exception
        Console.WriteLine(exc)
    End Try
End Function

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

public static async Task WhenAllExamineExceptions()
{
    Task[] asyncOps = (from addr in Stubs.addrs select Stubs.SendMailAsync(addr)).ToArray();
    try
    {
        await Task.WhenAll(asyncOps);
    }
    catch (Exception exc)
    {
        foreach (Task faulted in asyncOps.Where(t => t.IsFaulted))
        {
            Console.WriteLine($"Faulted: {faulted.Exception}");
        }
    }
}
Public Async Function WhenAllExamineExceptions() As Task
    Dim asyncOps As Task() = (From addr In Stubs.addrs Select Stubs.SendMailAsync(addr)).ToArray()
    Try
        Await Task.WhenAll(asyncOps)
    Catch exc As Exception
        For Each faulted As Task In asyncOps.Where(Function(t) t.IsFaulted)
            Console.WriteLine($"Faulted: {faulted.Exception}")
        Next
    End Try
End Function

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:

public static async Task WhenAllDownloadPagesExceptions()
{
    Task<string>[] asyncOps =
        (from url in Stubs.urls select Stubs.DownloadStringTaskAsync(url)).ToArray();
    try
    {
        string[] pages = await Task.WhenAll(asyncOps);
        Console.WriteLine(pages.Length);
    }
    catch (Exception exc)
    {
        foreach (Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
        {
            Console.WriteLine($"Faulted: {faulted.Exception}");
        }
    }
}
Public Async Function WhenAllDownloadPagesExceptions() As Task
    Dim asyncOps As Task(Of String)() =
        (From url In Stubs.urls Select Stubs.DownloadStringTaskAsync(url)).ToArray()
    Try
        Dim pages As String() = Await Task.WhenAll(asyncOps)
        Console.WriteLine(pages.Length)
    Catch exc As Exception
        For Each faulted As Task(Of String) In asyncOps.Where(Function(t) t.IsFaulted)
            Console.WriteLine($"Faulted: {faulted.Exception}")
        Next
    End Try
End Function

Task.WhenAny

WhenAny A metódussal aszinkron módon megvárhatja, amíg a tevékenységekké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őként befejezett művelet kiválasztása (például több tőzsdei árfolyam-webszolgáltatással való kapcsolatfelvétel, amelyek egyetlen eredményt adnak vissza, és kiválasztják azt, amelyik a leggyorsabb eredményt adja).

  • Interleaving: Több művelet elindítása és várakozás azok befejezésére, de azok befejeződésekor történő feldolgozása.

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

  • Korai mentés: A t1 feladat által képviselt művelet például csoportosítható egy WhenAny feladatba egy másik t2 feladattal, és várakozhat a WhenAny feladatra. 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 létezik, 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. WhenAny A metódussal értesítést kaphat, ha egy művelet befejeződik:

public static async Task WhenAnyRedundancy(string symbol)
{
    var recommendations = new List<Task<bool>>()
    {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    };
    Task<bool> recommendation = await Task.WhenAny(recommendations);
    if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyRedundancy(symbol As String) As Task
    Dim recommendations As New List(Of Task(Of Boolean)) From {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    }
    Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
    If Await recommendation Then Stubs.BuyStock(symbol)
End Function

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, dönthet úgy, hogy megvárja, míg a visszakapott feladat hibái megjelennek, és megfelelően kezelheti ezeket; például:

public static async Task WhenAnyRetryOnException(string symbol)
{
    Task<bool>[] allRecommendations = new Task<bool>[]
    {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    };
    var remaining = allRecommendations.ToList();
    while (remaining.Count > 0)
    {
        Task<bool> recommendation = await Task.WhenAny(remaining);
        try
        {
            if (await recommendation) Stubs.BuyStock(symbol);
            break;
        }
        catch (WebException)
        {
            remaining.Remove(recommendation);
        }
    }
}
Public Async Function WhenAnyRetryOnException(symbol As String) As Task
    Dim allRecommendations As Task(Of Boolean)() = {
        Stubs.GetBuyRecommendation1Async(symbol),
        Stubs.GetBuyRecommendation2Async(symbol),
        Stubs.GetBuyRecommendation3Async(symbol)
    }
    Dim remaining As List(Of Task(Of Boolean)) = allRecommendations.ToList()
    While remaining.Count > 0
        Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(remaining)
        Try
            If Await recommendation Then Stubs.BuyStock(symbol)
            Exit While
        Catch ex As WebException
            remaining.Remove(recommendation)
        End Try
    End While
End Function

Emellett még akkor is, ha egy első tevékenység sikeresen befejeződik, a későbbi tevékenységek sikertelenek lehetnek. Ezen a ponton számos lehetősége 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. Ebben a forgatókönyvben folytatásokat használva értesítést kaphat, ha a feladatok 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) { Stubs.Log(exc); }
    }
}
Private Async Sub LogCompletionIfFailed(tasks As IEnumerable(Of Task))
    For Each task In tasks
        Try
            Await task
        Catch exc As Exception
            Stubs.Log(exc)
        End Try
    Next
End Sub

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

public static async Task WhenAnyCancelRemainder(string symbol)
{
    var cts = new CancellationTokenSource();
    var recommendations = new List<Task<bool>>()
    {
        Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
    };

    Task<bool> recommendation = await Task.WhenAny(recommendations);
    cts.Cancel();
    if (await recommendation) Stubs.BuyStock(symbol);
}
Public Async Function WhenAnyCancelRemainder(symbol As String) As Task
    Dim cts As New CancellationTokenSource()
    Dim recommendations As New List(Of Task(Of Boolean)) From {
        Stubs.GetBuyRecommendation1Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation2Async(symbol, cts.Token),
        Stubs.GetBuyRecommendation3Async(symbol, cts.Token)
    }

    Dim recommendation As Task(Of Boolean) = Await Task.WhenAny(recommendations)
    cts.Cancel()
    If Await recommendation Then Stubs.BuyStock(symbol)
End Function

Váltakozás

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é, ha az összes kép letöltésére várna, mielőtt hozzáadja őket a felhasználói felülethez. Ehelyett a befejezésükkor szeretné hozzáadni őket.

public static async Task WhenAnyInterleaving(string[] imageUrls)
{
    List<Task<Bitmap>> imageTasks =
        (from imageUrl in imageUrls select Stubs.GetBitmapAsync(imageUrl)).ToList();
    while (imageTasks.Count > 0)
    {
        try
        {
            Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
            imageTasks.Remove(imageTask);

            Bitmap image = await imageTask;
            Console.WriteLine($"Got image: {image.Width}x{image.Height}");
        }
        catch { }
    }
}
Public Async Function WhenAnyInterleaving(imageUrls As String()) As Task
    Dim imageTasks As List(Of Task(Of Bitmap)) =
        (From imageUrl In imageUrls Select Stubs.GetBitmapAsync(imageUrl)).ToList()
    While imageTasks.Count > 0
        Try
            Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
            imageTasks.Remove(imageTask)

            Dim image As Bitmap = Await imageTask
            Console.WriteLine($"Got image: {image.Width}x{image.Height}")
        Catch
        End Try
    End While
End Function

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:

public static async Task WhenAnyInterleavingWithProcessing(string[] imageUrls)
{
    List<Task<Bitmap>> imageTasks =
        (from imageUrl in imageUrls
         select Stubs.GetBitmapAsync(imageUrl)
             .ContinueWith(t => Stubs.ConvertImage(t.Result))).ToList();
    while (imageTasks.Count > 0)
    {
        try
        {
            Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
            imageTasks.Remove(imageTask);

            Bitmap image = await imageTask;
            Console.WriteLine($"Got image: {image.Width}x{image.Height}");
        }
        catch { }
    }
}
Public Async Function WhenAnyInterleavingWithProcessing(imageUrls As String()) As Task
    Dim imageTasks As List(Of Task(Of Bitmap)) =
        (From imageUrl In imageUrls
         Select Stubs.GetBitmapAsync(imageUrl).ContinueWith(Function(t) Stubs.ConvertImage(t.Result))).ToList()
    While imageTasks.Count > 0
        Try
            Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
            imageTasks.Remove(imageTask)

            Dim image As Bitmap = Await imageTask
            Console.WriteLine($"Got image: {image.Width}x{image.Height}")
        Catch
        End Try
    End While
End Function

Fojtás

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

public static async Task WhenAnyThrottling(Uri[] uriList)
{
    const int CONCURRENCY_LEVEL = 15;
    int nextIndex = 0;
    var imageTasks = new List<Task<Bitmap>>();
    while (nextIndex < CONCURRENCY_LEVEL && nextIndex < uriList.Length)
    {
        imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
        nextIndex++;
    }

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

            Bitmap image = await imageTask;
            Console.WriteLine($"Got image: {image.Width}x{image.Height}");
        }
        catch (Exception exc) { Stubs.Log(exc); }

        if (nextIndex < uriList.Length)
        {
            imageTasks.Add(Stubs.GetBitmapAsync(uriList[nextIndex].ToString()));
            nextIndex++;
        }
    }
}
Public Async Function WhenAnyThrottling(uriList As Uri()) As Task
    Const CONCURRENCY_LEVEL As Integer = 15
    Dim nextIndex As Integer = 0
    Dim imageTasks As New List(Of Task(Of Bitmap))
    While nextIndex < CONCURRENCY_LEVEL AndAlso nextIndex < uriList.Length
        imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
        nextIndex += 1
    End While

    While imageTasks.Count > 0
        Try
            Dim imageTask As Task(Of Bitmap) = Await Task.WhenAny(imageTasks)
            imageTasks.Remove(imageTask)

            Dim image As Bitmap = Await imageTask
            Console.WriteLine($"Got image: {image.Width}x{image.Height}")
        Catch exc As Exception
            Stubs.Log(exc)
        End Try

        If nextIndex < uriList.Length Then
            imageTasks.Add(Stubs.GetBitmapAsync(uriList(nextIndex).ToString()))
            nextIndex += 1
        End If
    End While
End Function

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:

class EarlyBailoutUI
{
    private CancellationTokenSource? m_cts;

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

    public async void btnRun_Click(object sender, EventArgs e)
    {
        m_cts = new CancellationTokenSource();
        try
        {
            Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url");
            await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
            if (imageDownload.IsCompleted)
            {
                Bitmap image = await imageDownload;
                Stubs.Log(image);
            }
            else imageDownload.ContinueWith(t => Stubs.Log(t));
        }
        finally { }
    }
}
Class EarlyBailoutUI
    Private m_cts As CancellationTokenSource

    Public Sub btnCancel_Click(sender As Object, e As EventArgs)
        If m_cts IsNot Nothing Then m_cts.Cancel()
    End Sub

    Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
        m_cts = New CancellationTokenSource()
        Try
            Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url")
            Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
            If imageDownload.IsCompleted Then
                Dim image As Bitmap = Await imageDownload
                Stubs.Log(image)
            Else
                imageDownload.ContinueWith(Sub(t) Stubs.Log(t))
            End If
        Finally
        End Try
    End Sub
End Class

Ez az implementáció újra engedélyezi a felhasználói felületet, amint úgy dönt, hogy kilép, viszont 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 kilép, de a felhasználói felületet nem szabad újra létrehozni mindaddig, amíg a műveletek be nem fejeződnek, ami a lemondási kérelem miatti korai befejezés esetén korai lezárást jelenthet.

class EarlyBailoutWithTokenUI
{
    private CancellationTokenSource? m_cts;

    public async void btnRun_Click(object sender, EventArgs e)
    {
        m_cts = new CancellationTokenSource();
        try
        {
            Task<Bitmap> imageDownload = Stubs.GetBitmapAsync("url", m_cts.Token);
            await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token);
            Bitmap image = await imageDownload;
            Stubs.Log(image);
        }
        catch (OperationCanceledException) { }
        finally { }
    }
}
Class EarlyBailoutWithTokenUI
    Private m_cts As CancellationTokenSource

    Public Async Sub btnRun_Click(sender As Object, e As EventArgs)
        m_cts = New CancellationTokenSource()
        Try
            Dim imageDownload As Task(Of Bitmap) = Stubs.GetBitmapAsync("url", m_cts.Token)
            Await Examples.UntilCompletionOrCancellation(imageDownload, m_cts.Token)
            Dim image As Bitmap = Await imageDownload
            Stubs.Log(image)
        Catch ex As OperationCanceledException
        Finally
        End Try
    End Sub
End Class

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 függvény

Task.Delay A metódussal szüneteltetéseket adhat egy aszinkron metódus végrehajtásához. Ez a szüneteltetés 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. Az Task.Delay metódussal a Task.WhenAny-tel időtúllépéseket valósíthat meg a várakozások esetén.

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 képessé váljunk az időkorlát beállítására, mikor az aszinkron műveletre várakozunk. A szinkron Task.Waités Task.WaitAllTask.WaitAnya metódusok időtúllépési értékeket fogadnak el, de a megfelelőTaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny és a korábban említett Task.WhenAll/Task.WhenAny metódusok nem. Ehelyett használjon Task.Delay és Task.WhenAny együtt időtúllépési mechanizmust.

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 static async Task<Bitmap?> DownloadWithTimeout(string url)
{
    Task<Bitmap> download = Stubs.GetBitmapAsync(url);
    if (download == await Task.WhenAny(download, Task.Delay(3000)))
    {
        return await download;
    }
    else
    {
        var ignored = download.ContinueWith(
            t => Trace($"Task finally completed: {t.Status}"));
        return null;
    }
}

static void Trace(string message) => Console.WriteLine(message);
Public Async Function DownloadWithTimeout(url As String) As Task(Of Bitmap)
    Dim download As Task(Of Bitmap) = Stubs.GetBitmapAsync(url)
    If download Is Await Task.WhenAny(download, Task.Delay(3000)) Then
        Return Await download
    Else
        Dim ignored = download.ContinueWith(Sub(t) TraceMsg($"Task finally completed: {t.Status}"))
        Return Nothing
    End If
End Function

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

public static async Task<Bitmap[]?> DownloadMultipleWithTimeout(string[] imageUrls)
{
    Task<Bitmap[]> downloads =
        Task.WhenAll(from url in imageUrls select Stubs.GetBitmapAsync(url));
    if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
    {
        return await downloads;
    }
    else
    {
        downloads.ContinueWith(t => Stubs.Log(t));
        return null;
    }
}
Public Async Function DownloadMultipleWithTimeout(imageUrls As String()) As Task(Of Bitmap())
    Dim downloads As Task(Of Bitmap()) =
        Task.WhenAll(From url In imageUrls Select Stubs.GetBitmapAsync(url))
    If downloads Is Await Task.WhenAny(downloads, Task.Delay(3000)) Then
        Return Await downloads
    Else
        downloads.ContinueWith(Sub(t) Stubs.Log(t))
        Return Nothing
    End If
End Function

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 újra meg kell próbálnia egy műveletet, ha egy korábbi kísérlet meghiúsul. Szinkron kód esetén létrehozhat egy segédmetódust, például RetryOnFault a következő példában a feladat elvégzéséhez:

public static T RetryOnFault<T>(Func<T> function, int maxTries)
{
    for (int i = 0; i < maxTries; i++)
    {
        try { return function(); }
        catch { if (i == maxTries - 1) throw; }
    }
    return default(T)!;
}
Public Function RetryOnFaultSync(Of T)(func As Func(Of T), maxTries As Integer) As T
    For i As Integer = 0 To maxTries - 1
        Try
            Return func()
        Catch
            If i = maxTries - 1 Then Throw
        End Try
    Next
    Return Nothing
End Function

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)!;
}
Public Async Function RetryOnFault(Of T)(func As Func(Of Task(Of T)), maxTries As Integer) As Task(Of T)
    For i As Integer = 0 To maxTries - 1
        Try
            Return Await func().ConfigureAwait(False)
        Catch
            If i = maxTries - 1 Then Throw
        End Try
    Next
    Return Nothing
End Function

Ezt a kombinatort használva újrakódolhatja az újrapróbálkozásokat az alkalmazás logikájába. 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 RetryOnFault tovább bővítheti. A függvény például elfogadhat egy másikat Func<Task> , amelyet a függvény újrapróbálkozások 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> RetryOnFaultWithDelay<T>(
    Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
    for (int i = 0; i < maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries - 1) throw; }
        await retryWhen().ConfigureAwait(false);
    }
    return default(T)!;
}
Public Async Function RetryOnFaultWithDelay(Of T)(
    func As Func(Of Task(Of T)), maxTries As Integer, retryWhen As Func(Of Task)) As Task(Of T)
    For i As Integer = 0 To maxTries - 1
        Try
            Return Await func().ConfigureAwait(False)
        Catch
            If i = maxTries - 1 Then Throw
        End Try
        Await retryWhen().ConfigureAwait(False)
    Next
    Return Nothing
End Function

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 az egyes szolgáltatások különböző minőségi és válaszidőket biztosíthatnak. Az ingadozások kezelése érdekében kéréseket adhat ki az összes webszolgáltatásnak, és amint választ kap az egyiktő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<T>(
    params Func<CancellationToken, Task<T>>[] functions)
{
    var cts = new CancellationTokenSource();
    var tasks = (from function in functions
                 select function(cts.Token)).ToArray();
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach (var task in tasks)
    {
        var ignored = task.ContinueWith(
            t => Stubs.Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return await completed;
}
Public Async Function NeedOnlyOne(Of T)(
    ParamArray functions As Func(Of CancellationToken, Task(Of T))()) As Task(Of T)
    Dim cts As New CancellationTokenSource()
    Dim tasks As Task(Of T)() = (From func In functions Select func(cts.Token)).ToArray()
    Dim completed As Task(Of T) = Await Task.WhenAny(tasks).ConfigureAwait(False)
    cts.Cancel()
    For Each task In tasks
        Dim ignored = task.ContinueWith(
            Sub(tsk) Stubs.Log(tsk), TaskContinuationOptions.OnlyOnFaulted)
    Next
    Return Await completed
End Function

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

Közbenső műveletek

Ha a WhenAny metódussal támogat egy összefüggő forgatókönyvet, teljesítményproblémát okozhat, ha nagy feladatkészletekkel dolgozik. Minden egyes hívás regisztrálja a WhenAny folytatást az egyes tevékenységekhez. Az N számú tevékenység esetében ez a folyamat O(N2) folytatásokat hoz létre az összekötő művelet élettartama alatt. Ha nagy feladatkészlettel dolgozik, használjon egy kombinátort (Interleaved az alábbi példában) a teljesítményproblémák megoldásához:

public static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
    var inputTasks = tasks.ToList();
    var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
                   select new TaskCompletionSource<T>()).ToList();
    int nextTaskIndex = -1;
    foreach (var inputTask in inputTasks)
    {
        inputTask.ContinueWith(completed =>
        {
            var source = sources[Interlocked.Increment(ref nextTaskIndex)];
            if (completed.IsFaulted)
                source.TrySetException(completed.Exception!.InnerExceptions);
            else if (completed.IsCanceled)
                source.TrySetCanceled();
            else
                source.TrySetResult(completed.Result);
        }, CancellationToken.None,
           TaskContinuationOptions.ExecuteSynchronously,
           TaskScheduler.Default);
    }
    return from source in sources
           select source.Task;
}
Public Function Interleaved(Of T)(tasks As IEnumerable(Of Task(Of T))) As IEnumerable(Of Task(Of T))
    Dim inputTasks As List(Of Task(Of T)) = tasks.ToList()
    Dim sources As List(Of TaskCompletionSource(Of T)) =
        (From _i In Enumerable.Range(0, inputTasks.Count) Select New TaskCompletionSource(Of T)()).ToList()
    Dim indexRef As Integer() = {-1}
    For Each inputTask In inputTasks
        inputTask.ContinueWith(Sub(completed)
                                   Dim idx = Interlocked.Increment(indexRef(0))
                                   Dim source = sources(idx)
                                   If completed.IsFaulted Then
                                       source.TrySetException(completed.Exception.InnerExceptions)
                                   ElseIf completed.IsCanceled Then
                                       source.TrySetCanceled()
                                   Else
                                       source.TrySetResult(completed.Result)
                                   End If
                               End Sub,
                               CancellationToken.None,
                               TaskContinuationOptions.ExecuteSynchronously,
                               TaskScheduler.Default)
    Next
    Return From source In sources Select source.Task
End Function

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 elosztó/gyűjtő forgatókönyvekben érdemes lehet megvárni a készlet összes feladatának befejezését, kivéve, ha az egyik hibát okoz. Ebben az esetben a kivétel bekövetkezése után azonnal le szeretné állítani a várakozást. Ezt a viselkedést 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 => ((Task<T>)t).Result).ToArray());
    };

    foreach (var t in inputs) t.ContinueWith(onCompleted);
    return tcs.Task;
}
Public Function WhenAllOrFirstException(Of T)(tasks As IEnumerable(Of Task(Of T))) As Task(Of T())
    Dim inputs As List(Of Task(Of T)) = tasks.ToList()
    Dim ce As New CountdownEvent(inputs.Count)
    Dim tcs As New TaskCompletionSource(Of T())()

    Dim onCompleted As Action(Of Task) = Sub(completed As Task)
                                             If completed.IsFaulted Then
                                                 tcs.TrySetException(completed.Exception.InnerExceptions)
                                             End If
                                             If ce.Signal() AndAlso Not tcs.Task.IsCompleted Then
                                                 tcs.TrySetResult(inputs.Select(Function(taskItem) DirectCast(taskItem, Task(Of T)).Result).ToArray())
                                             End If
                                         End Sub

    For Each t In inputs
        t.ContinueWith(onCompleted)
    Next
    Return tcs.Task
End Function

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 felhasználónak is átadhatja. Minden fogyasztó megvárhatja, vele regisztrálhatja a folytatásokat, megkaphatja annak eredményét vagy kivételeit (például Task<TResult> esetén), és így tovább. Ez a tényező teszi Task és Task<TResult> kiválóan alkalmassá 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 a Task<TResult>-ra épül.

public class AsyncCache<TKey, TValue> where TKey : notnull
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

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

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException(nameof(key));
            return _map.GetOrAdd(key, toAdd =>
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}
Public Class AsyncCache(Of TKey, TValue)
    Private ReadOnly _valueFactory As Func(Of TKey, Task(Of TValue))
    Private ReadOnly _map As New ConcurrentDictionary(Of TKey, Lazy(Of Task(Of TValue)))()

    Public Sub New(valueFactory As Func(Of TKey, Task(Of TValue)))
        If valueFactory Is Nothing Then Throw New ArgumentNullException(NameOf(valueFactory))
        _valueFactory = valueFactory
    End Sub

    Default Public ReadOnly Property Item(key As TKey) As Task(Of TValue)
        Get
            If key Is Nothing Then Throw New ArgumentNullException(NameOf(key))
            Return _map.GetOrAdd(key, Function(toAdd) New Lazy(Of Task(Of TValue))(Function() _valueFactory(toAdd))).Value
        End Get
    End Property
End Class

Az AsyncCache<TKey,TValue> osztály a konstruktorában egy olyan függvényt fogad el delegáltként, amelyhez egy TKey paramétert kap, és egy Task<TResult> értéket ad vissza. A belső szótár a gyorsítótárból tárolja a korábban elért értékeket, és AsyncCache biztosítja, hogy kulcsonként csak egy feladatot hozzon létre, 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.

static AsyncCache<string, string> m_webPages =
    new AsyncCache<string, string>(url => Stubs.DownloadStringTaskAsync(url));

public static async Task UseWebPageCache(string url)
{
    string contents = await m_webPages[url];
    Console.WriteLine(contents.Length);
}
Private m_webPages As New AsyncCache(Of String, String)(Function(url) Stubs.DownloadStringTaskAsync(url))

Public Async Function UseWebPageCache(url As String) As Task
    Dim contents As String = Await m_webPages(url)
    Console.WriteLine(contents.Length)
End Function

AszinkronTermelőFogyasztóGyűjtemény

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;
            }
        }
    }
}
Public Class AsyncProducerConsumerCollection(Of T)
    Private ReadOnly m_collection As New Queue(Of T)()
    Private ReadOnly m_waiting As New Queue(Of TaskCompletionSource(Of T))()

    Public Sub Add(item As T)
        Dim tcs As TaskCompletionSource(Of T) = Nothing
        SyncLock m_collection
            If m_waiting.Count > 0 Then
                tcs = m_waiting.Dequeue()
            Else
                m_collection.Enqueue(item)
            End If
        End SyncLock
        If tcs IsNot Nothing Then tcs.TrySetResult(item)
    End Sub

    Public Function Take() As Task(Of T)
        SyncLock m_collection
            If m_collection.Count > 0 Then
                Return Task.FromResult(m_collection.Dequeue())
            Else
                Dim tcs As New TaskCompletionSource(Of T)()
                m_waiting.Enqueue(tcs)
                Return tcs.Task
            End If
        End SyncLock
    End Function
End Class

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

static AsyncProducerConsumerCollection<int> m_data = new();

public static async Task ConsumerAsync()
{
    while (true)
    {
        int nextItem = await m_data.Take();
        Stubs.ProcessNextItem(nextItem);
    }
}

public static void Produce(int data)
{
    m_data.Add(data);
}
Private m_data As New AsyncProducerConsumerCollection(Of Integer)()

Public Async Function ConsumerAsync() As Task
    While True
        Dim nextItem As Integer = Await m_data.Take()
        Stubs.ProcessNextItem(nextItem)
    End While
End Function

Public Sub Produce(data As Integer)
    m_data.Add(data)
End Sub

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:

static BufferBlock<int> m_dataBlock = new();

public static async Task ConsumerAsyncBlock()
{
    while (true)
    {
        int nextItem = await m_dataBlock.ReceiveAsync();
        Stubs.ProcessNextItem(nextItem);
    }
}

public static void ProduceBlock(int data)
{
    m_dataBlock.Post(data);
}
Private m_dataBlock As New BufferBlock(Of Integer)()

Public Async Function ConsumerAsyncBlock() As Task
    While True
        Dim nextItem As Integer = Await m_dataBlock.ReceiveAsync()
        Stubs.ProcessNextItem(nextItem)
    End While
End Function

Public Sub ProduceBlock(data As Integer)
    m_dataBlock.Post(data)
End Sub

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