Share via


Använda det aktivitetsbaserade asynkrona mönstret

När du använder det aktivitetsbaserade Asynkrona mönstret (TAP) för att arbeta med asynkrona åtgärder kan du använda motringningar för att uppnå väntan utan att blockera. För uppgifter uppnås detta genom metoder som Task.ContinueWith. Språkbaserat asynkront stöd döljer återanrop genom att tillåta att asynkrona åtgärder väntar i det normala kontrollflödet, och kompilatorgenererad kod ger samma stöd på API-nivå.

Pausa körning med Await

Du kan använda nyckelordet await i C# och Await Operator i Visual Basic för att asynkront vänta Task och Task<TResult> objekt. När du väntar på en Taskawait är uttrycket av typen void. När du väntar på en Task<TResult>await är uttrycket av typen TResult. Ett await uttryck måste förekomma i en asynkron metods brödtext. (Dessa språkfunktioner introducerades i .NET Framework 4.5.)

Under täcket installerar await-funktionen ett återanrop för uppgiften med hjälp av en fortsättning. Återanropet återupptar den asynkrona metoden vid tidpunkten för avstängningen. När den asynkrona metoden återupptas, om den förväntade åtgärden har slutförts och var en Task<TResult>, returneras dess TResult . Om eller TaskTask<TResult> som väntades avslutades i tillståndet Canceled utlöses ett OperationCanceledException undantag. Om eller TaskTask<TResult> som väntades slutade i Faulted tillståndet utlöses undantaget som orsakade felet. En Task kan fel som ett resultat av flera undantag, men endast ett av dessa undantag sprids. Egenskapen returnerar dock Task.Exception ett AggregateException undantag som innehåller alla fel.

Om en synkroniseringskontext (SynchronizationContext objekt) är associerad med tråden som körde den asynkrona metoden vid tidpunkten för avstängningen (till exempel om SynchronizationContext.Current egenskapen inte nullär ), återupptas den asynkrona metoden i samma synkroniseringskontext med hjälp av kontextens Post metod. Annars förlitar den sig på schemaläggaren (TaskScheduler objektet) som var aktuell vid tidpunkten för avstängningen. Vanligtvis är detta standard schemaläggaren (TaskScheduler.Default), som riktar sig till trådpoolen. Den här schemaläggaren avgör om den förväntade asynkrona åtgärden ska återupptas där den slutfördes eller om återupptagandet ska schemaläggas. Standardschemaläggaren tillåter vanligtvis fortsättningen att köras på den tråd som den väntade åtgärden slutförde.

När en asynkron metod anropas körs funktionens brödtext synkront fram till det första inväntningsuttrycket på en väntande instans som ännu inte har slutförts. Då återgår anropet till anroparen. Om den asynkrona metoden inte returnerar voidreturneras ett Task eller Task<TResult> -objekt för att representera den pågående beräkningen. Om en retursats påträffas eller slutet av metodtexten nås i en icke-void-asynkron metod slutförs uppgiften i det RanToCompletion slutliga tillståndet. Om ett ohanterat undantag gör att kontrollen lämnar den asynkrona metodens brödtext slutar aktiviteten i tillståndet Faulted . Om undantaget är ett OperationCanceledExceptionslutar aktiviteten i stället i Canceled tillståndet . På så sätt publiceras resultatet eller undantaget så småningom.

Det finns flera viktiga varianter av det här beteendet. Av prestandaskäl, om en aktivitet redan har slutförts när aktiviteten väntar, returneras inte kontrollen och funktionen fortsätter att köras. Att återgå till den ursprungliga kontexten är dessutom inte alltid det önskade beteendet och kan ändras. Detta beskrivs mer detaljerat i nästa avsnitt.

Konfigurera avstängning och återupptagning med Yield och ConfigureAwait

Flera metoder ger mer kontroll över körningen av en asynkron metod. Du kan till exempel använda Task.Yield metoden för att introducera en avkastningspunkt i den asynkrona metoden:

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

Detta motsvarar att asynkront publicera eller schemalägga tillbaka till den aktuella kontexten.

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

Du kan också använda Task.ConfigureAwait metoden för bättre kontroll över fjädring och återupptagning i en asynkron metod. Som tidigare nämnts avbildas den aktuella kontexten som standard när en asynkron metod pausas och den insamlade kontexten används för att anropa den asynkrona metodens fortsättning vid återupptagande. I många fall är detta det exakta beteendet du vill ha. I andra fall kanske du inte bryr dig om fortsättningskontexten, och du kan uppnå bättre prestanda genom att undvika sådana inlägg tillbaka till den ursprungliga kontexten. Om du vill aktivera detta använder du Task.ConfigureAwait metoden för att informera inväntningsåtgärden om att inte avbilda och återuppta kontexten, utan för att fortsätta körningen oavsett var den asynkrona åtgärd som väntades slutföras:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Avbryta en asynkron åtgärd

Från och med .NET Framework 4 ger TAP-metoder som stöder annullering minst en överlagring som accepterar en annulleringstoken (CancellationToken -objekt).

En annulleringstoken skapas via en annulleringstokenkälla (CancellationTokenSource objekt). Källans egenskap returnerar den annulleringstoken Token som ska signaleras när källans metod anropas Cancel . Om du till exempel vill ladda ned en enskild webbsida och vill kunna avbryta åtgärden skapar du ett CancellationTokenSource objekt, skickar dess token till TAP-metoden och anropar sedan källans Cancel metod när du är redo att avbryta åtgärden:

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

Om du vill avbryta flera asynkrona anrop kan du skicka samma token till alla anrop:

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

Eller så kan du skicka samma token till en selektiv delmängd av åtgärder:

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

Viktigt!

Annulleringsbegäranden kan initieras från valfri tråd.

Du kan skicka CancellationToken.None värdet till vilken metod som helst som accepterar en annulleringstoken för att ange att annullering aldrig kommer att begäras. Detta gör CancellationToken.CanBeCanceled att egenskapen returnerar false, och den anropade metoden kan optimera i enlighet med detta. För testningsändamål kan du också skicka in en förinställd annulleringstoken som instansieras med hjälp av konstruktorn som accepterar ett booleskt värde för att ange om token ska starta i ett redan avbrutet eller inte avbrutet tillstånd.

Den här metoden för annullering har flera fördelar:

  • Du kan skicka samma annulleringstoken till valfritt antal asynkrona och synkrona åtgärder.

  • Samma begäran om avbokning kan spridas till valfritt antal lyssnare.

  • Utvecklaren av det asynkrona API:et har fullständig kontroll över om annullering kan begäras och när det kan träda i kraft.

  • Koden som använder API:et kan selektivt fastställa de asynkrona anrop som annulleringsbegäranden ska spridas till.

Övervakningsstatus

Vissa asynkrona metoder exponerar förloppet via ett förloppsgränssnitt som skickas till den asynkrona metoden. Tänk dig till exempel en funktion som asynkront laddar ned en textsträng och på vägen genererar förloppsuppdateringar som inkluderar procentandelen av nedladdningen som har slutförts hittills. En sådan metod kan användas i ett WPF-program (Windows Presentation Foundation) på följande sätt:

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

Använda de inbyggda aktivitetsbaserade kombinatorerna

Namnområdet System.Threading.Tasks innehåller flera metoder för att skapa och arbeta med uppgifter.

Task.Run

Klassen Task innehåller flera Run metoder som gör att du enkelt kan avlasta arbete som en Task eller Task<TResult> till trådpoolen, till exempel:

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

Vissa av dessa Run metoder, till exempel överlagringen Task.Run(Func<Task>) , finns som förkortning för TaskFactory.StartNew metoden. Med den här överlagringen kan du använda await i det avlastade arbetet, till exempel:

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

Sådana överlagringar är logiskt likvärdiga med att använda TaskFactory.StartNew metoden tillsammans med Unwrap tilläggsmetoden i det parallella aktivitetsbiblioteket.

Task.FromResult

FromResult Använd metoden i scenarier där data kanske redan är tillgängliga och bara behöver returneras från en uppgiftsreturmetod som lyfts till en Task<TResult>:

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 Använd metoden för att asynkront vänta på flera asynkrona åtgärder som representeras som uppgifter. Metoden har flera överlagringar som stöder en uppsättning icke-generiska uppgifter eller en icke-enhetlig uppsättning generiska uppgifter (till exempel asynkront väntar på flera åtgärder som returneras utan tomrum eller asynkront väntar på flera metoder för värderetur där varje värde kan ha en annan typ) och för att stödja en enhetlig uppsättning allmänna uppgifter (till exempel asynkront väntar på flera TResult-returning-metoder).

Anta att du vill skicka e-postmeddelanden till flera kunder. Du kan överlappa att skicka meddelandena så att du inte väntar på att ett meddelande ska slutföras innan du skickar nästa. Du kan också ta reda på när sändningsåtgärderna har slutförts och om några fel har inträffat:

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

Den här koden hanterar inte uttryckligen undantag som kan inträffa, men tillåter att undantag sprids ut från await den resulterande aktiviteten från WhenAll. Om du vill hantera undantagen kan du använda kod som följande:

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

I det här fallet, om någon asynkron åtgärd misslyckas, konsolideras alla undantag i ett AggregateException undantag, som lagras i Task som returneras från WhenAll metoden. Endast ett av dessa undantag sprids dock av nyckelordet await . Om du vill undersöka alla undantag kan du skriva om den tidigare koden på följande sätt:

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

Låt oss överväga ett exempel på hur du laddar ned flera filer från webben asynkront. I det här fallet har alla asynkrona åtgärder homogena resultattyper och det är enkelt att komma åt resultaten:

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

Du kan använda samma metoder för undantagshantering som vi diskuterade i det tidigare scenariot med void-returning:

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

Du kan använda WhenAny metoden för att asynkront vänta på att bara en av flera asynkrona åtgärder som representeras som uppgifter ska slutföras. Den här metoden hanterar fyra primära användningsfall:

  • Redundans: Utför en åtgärd flera gånger och väljer den som slutförs först (till exempel genom att kontakta flera webbtjänster för aktieofferter som ger ett enda resultat och väljer den som slutförs snabbast).

  • Interleaving: Starta flera åtgärder och vänta på att alla ska slutföras, men bearbeta dem när de slutförs.

  • Begränsning: Gör att ytterligare åtgärder kan börja när andra slutförs. Det här är en förlängning av interleaving-scenariot.

  • Tidig räddningsåtgärd: En åtgärd som representeras av aktivitet t1 kan till exempel grupperas i en aktivitet med en WhenAny annan uppgift t2 och du kan vänta på WhenAny aktiviteten. Uppgift t2 kan representera en timeout eller annullering eller någon annan signal som gör WhenAny att aktiviteten slutförs innan t1 slutförs.

Redundans

Överväg ett fall där du vill fatta ett beslut om du vill köpa en aktie. Det finns flera webbtjänster för aktierekommendationer som du litar på, men beroende på den dagliga belastningen kan varje tjänst bli långsam vid olika tidpunkter. Du kan använda WhenAny metoden för att ta emot ett meddelande när en åtgärd har slutförts:

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

Till skillnad från WhenAll, som returnerar de oöppnade resultaten för alla aktiviteter som har slutförts, WhenAny returnerar den uppgift som har slutförts. Om en aktivitet misslyckas är det viktigt att veta att den misslyckades, och om en aktivitet lyckas är det viktigt att veta vilken uppgift som returvärdet är associerat med. Därför måste du komma åt resultatet av den returnerade aktiviteten, eller invänta den ytterligare, som det här exemplet visar.

Precis som med WhenAllmåste du kunna hantera undantag. Eftersom du får tillbaka den slutförda uppgiften kan du invänta att den returnerade aktiviteten får fel som sprids, och try/catch dem på rätt sätt, till exempel:

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

Även om en första uppgift slutförs kan efterföljande aktiviteter dessutom misslyckas. Nu har du flera alternativ för att hantera undantag: Du kan vänta tills alla startuppgifter har slutförts, i vilket fall du kan använda WhenAll metoden, eller så kan du bestämma att alla undantag är viktiga och måste loggas. För detta kan du använda fortsättningar för att få ett meddelande när aktiviteterna har slutförts asynkront:

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

eller:

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

eller till och med:

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

Slutligen kanske du vill avbryta alla återstående åtgärder:

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

Interfoliering

Tänk dig ett fall där du laddar ned bilder från webben och bearbetar varje bild (till exempel lägga till bilden i en användargränssnittskontroll). Du bearbetar bilderna sekventiellt i användargränssnittstråden, men vill ladda ned bilderna så samtidigt som möjligt. Du vill inte heller lägga till bilderna i användargränssnittet förrän alla laddas ned. I stället vill du lägga till dem när de har slutförts.

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

Du kan också tillämpa interleaving på ett scenario som omfattar beräkningsintensiv bearbetning på de ThreadPool nedladdade bilderna, till exempel:

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

Begränsning

Överväg interleaving-exemplet, förutom att användaren laddar ned så många bilder att nedladdningarna måste begränsas. Du vill till exempel bara att ett visst antal nedladdningar ska ske samtidigt. För att uppnå detta kan du starta en delmängd av de asynkrona åtgärderna. När åtgärderna har slutförts kan du starta ytterligare åtgärder för att utföra dem:

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

Tidig räddningsaktion

Tänk på att du väntar asynkront på att en åtgärd ska slutföras samtidigt som du svarar på en användares annulleringsbegäran (till exempel klickade användaren på en avbryt-knapp). Följande kod illustrerar det här scenariot:

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

Den här implementeringen återaktiver användargränssnittet så snart du bestämmer dig för att lösa ut, men avbryter inte de underliggande asynkrona åtgärderna. Ett annat alternativ är att avbryta väntande åtgärder när du bestämmer dig för att lösa ut, men inte återupprätta användargränssnittet förrän åtgärderna har slutförts, eventuellt på grund av att de slutar tidigt på grund av annulleringsbegäran:

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

Ett annat exempel på tidig räddningsaktion är att använda WhenAny metoden tillsammans med Delay metoden, enligt beskrivningen i nästa avsnitt.

Task.Delay

Du kan använda Task.Delay metoden för att introducera pauser i körningen av en asynkron metod. Detta är användbart för många typer av funktioner, inklusive att skapa avsökningsslingor och fördröja hanteringen av användarindata under en fördefinierad tidsperiod. Metoden Task.Delay kan också vara användbar i kombination med Task.WhenAny för implementering av tidsgränser i väntan.

Om en aktivitet som ingår i en större asynkron åtgärd (till exempel en ASP.NET webbtjänst) tar för lång tid att slutföra kan den övergripande åtgärden bli lidande, särskilt om den inte kan slutföras. Därför är det viktigt att du kan överskrida tidsgränsen när du väntar på en asynkron åtgärd. De synkrona Task.Waitmetoderna , Task.WaitAlloch Task.WaitAny accepterar timeout-värden, men motsvarande/TaskFactory.ContinueWhenAnyTaskFactory.ContinueWhenAlloch de tidigare nämnda Task.WhenAll/Task.WhenAny metoderna gör det inte. I stället kan du använda Task.Delay och Task.WhenAny i kombination för att implementera en timeout.

Anta till exempel i ditt användargränssnittsprogram att du vill ladda ned en bild och inaktivera användargränssnittet medan avbildningen laddas ned. Men om nedladdningen tar för lång tid vill du återaktivera användargränssnittet och ignorera nedladdningen:

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

Samma sak gäller för flera nedladdningar eftersom WhenAll returnerar en uppgift:

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

Skapa aktivitetsbaserade kombinatorer

Eftersom en uppgift kan representera en asynkron åtgärd helt och hållet och tillhandahålla synkrona och asynkrona funktioner för att ansluta till åtgärden, hämta dess resultat och så vidare, kan du skapa användbara bibliotek med kombinatorer som skapar uppgifter för att skapa större mönster. Som beskrivs i föregående avsnitt innehåller .NET flera inbyggda kombinatorer, men du kan också skapa egna. Följande avsnitt innehåller flera exempel på potentiella kombinatormetoder och typer.

RetryOnFault

I många situationer kanske du vill försöka utföra en åtgärd igen om ett tidigare försök misslyckas. För synkron kod kan du skapa en hjälpmetod, till exempel RetryOnFault i följande exempel för att åstadkomma detta:

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

Du kan skapa en nästan identisk hjälpmetod för asynkrona åtgärder som implementeras med TAP och därmed returnera uppgifter:

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

Du kan sedan använda den här kombinatorn för att koda omförsök till programmets logik. till exempel:

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

Du kan utöka RetryOnFault funktionen ytterligare. Funktionen kan till exempel acceptera en annan Func<Task> som anropas mellan återförsök för att avgöra när åtgärden ska utföras igen, till exempel:

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

Du kan sedan använda funktionen på följande sätt för att vänta en sekund innan du försöker utföra åtgärden igen:

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

Ibland kan du dra nytta av redundans för att förbättra en åtgärds svarstid och chanser att lyckas. Överväg flera webbtjänster som tillhandahåller aktiekurser, men vid olika tidpunkter på dagen kan varje tjänst ge olika nivåer av kvalitet och svarstider. För att hantera dessa fluktuationer kan du utfärda begäranden till alla webbtjänster och så snart du får ett svar från en av dem kan du avbryta de återstående begärandena. Du kan implementera en hjälpfunktion för att göra det enklare att implementera det här vanliga mönstret för att starta flera åtgärder, vänta på alla och sedan avbryta resten. Funktionen NeedOnlyOne i följande exempel illustrerar det här scenariot:

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

Du kan sedan använda den här funktionen på följande sätt:

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

Interleaved-åtgärder

Det finns ett potentiellt prestandaproblem med att använda WhenAny metoden för att stödja ett interfolieringsscenario när du arbetar med stora uppsättningar uppgifter. Varje anrop till resulterar i att WhenAny en fortsättning registreras för varje aktivitet. För N antal aktiviteter resulterar detta i O(N2) fortsättningar som skapats under livslängden för interfolieringsåtgärden. Om du arbetar med en stor uppsättning uppgifter kan du använda en kombinator (Interleaved i följande exempel) för att åtgärda prestandaproblemet:

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

Du kan sedan använda kombinatorn för att bearbeta resultatet av aktiviteter när de slutförs. till exempel:

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

WhenAllOrFirstException

I vissa punkt-/insamlingsscenarier kanske du vill vänta på alla uppgifter i en uppsättning, såvida inte ett av dem är fel, i vilket fall du vill sluta vänta så snart undantaget inträffar. Du kan göra det med en kombinatormetod, WhenAllOrFirstException till exempel i följande exempel:

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

Skapa uppgiftsbaserade datastrukturer

Förutom möjligheten att skapa anpassade uppgiftsbaserade kombinatorer gör en datastruktur i Task och Task<TResult> som representerar både resultatet av en asynkron åtgärd och den synkronisering som krävs för att ansluta till den en kraftfull typ som du kan skapa anpassade datastrukturer som ska användas i asynkrona scenarier.

AsyncCache

En viktig aspekt av en uppgift är att den kan delas ut till flera konsumenter, som alla kan vänta på den, registrera fortsättningar med den, få sitt resultat eller undantag (i fallet Task<TResult>med ), och så vidare. Detta gör Task och Task<TResult> passar perfekt för att användas i en asynkron cachelagringsinfrastruktur. Här är ett exempel på en liten men kraftfull asynkron cache som bygger på Task<TResult>:

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

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

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

Klassen AsyncCache<TKey,TValue> accepterar som ett ombud till konstruktorn en funktion som tar en TKey och returnerar en Task<TResult>. Alla tidigare använda värden från cacheminnet lagras i den interna ordlistan och AsyncCache säkerställer att endast en uppgift genereras per nyckel, även om cachen används samtidigt.

Du kan till exempel skapa en cache för nedladdade webbsidor:

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

Du kan sedan använda den här cachen i asynkrona metoder när du behöver innehållet på en webbsida. Klassen AsyncCache ser till att du laddar ned så få sidor som möjligt och cachelagrar resultatet.

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

Du kan också använda uppgifter för att skapa datastrukturer för att samordna asynkrona aktiviteter. Överväg ett av de klassiska parallella designmönstren: producent/konsument. I det här mönstret genererar producenterna data som förbrukas av konsumenterna, och producenterna och konsumenterna kan köras parallellt. Till exempel bearbetar konsumenten objekt 1, som tidigare genererades av en producent som nu producerar objekt 2. För producent-/konsumentmönstret behöver du alltid viss datastruktur för att lagra det arbete som skapats av producenterna så att konsumenterna kan meddelas om nya data och hitta dem när de är tillgängliga.

Här är en enkel datastruktur som bygger på uppgifter som gör att asynkrona metoder kan användas som producenter och konsumenter:

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

Med datastrukturen på plats kan du skriva kod, till exempel följande:

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

Namnområdet System.Threading.Tasks.Dataflow innehåller den BufferBlock<T> typ som du kan använda på ett liknande sätt, men utan att behöva skapa en anpassad samlingstyp:

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

Kommentar

Namnområdet System.Threading.Tasks.Dataflow är tillgängligt som ett NuGet-paket. Om du vill installera sammansättningen som innehåller System.Threading.Tasks.Dataflow namnområdet öppnar du projektet i Visual Studio, väljer Hantera NuGet-paket på Project-menyn och söker online efter System.Threading.Tasks.Dataflow paketet.

Se även