Delen via


Het Asynchrone taakgebaseerde patroon gebruiken

Wanneer u het op taken gebaseerde Asynchrone patroon (TAP) gebruikt om te werken met asynchrone bewerkingen, kunt u callbacks gebruiken om te wachten zonder te blokkeren. Voor taken wordt dit bereikt via methoden zoals Task.ContinueWith. Taalgebaseerde asynchrone ondersteuning verbergt callbacks door het mogelijk te maken dat op asynchrone bewerkingen binnen de normale controleflow kan worden gewacht, en door door de compiler gegenereerde code wordt dezelfde ondersteuning op API-niveau geboden.

Onderbreken van uitvoering met Await

U kunt het trefwoord await in C# en de Await Operator in Visual Basic gebruiken om asynchroon te wachten Task en Task<TResult> objecten. Wanneer u wacht op een Task, is de await expressie van het type void. Wanneer u wacht op een Task<TResult>, is de await expressie van het type TResult. Een await expressie moet voorkomen in de hoofdtekst van een asynchrone methode. (Deze taalfuncties zijn geïntroduceerd in .NET Framework 4.5.)

Achter de schermen installeert de await functionaliteit een callback voor de taak door middel van een voortzetting. Deze callback hervat de asynchrone methode op het punt van onderbreking. Wanneer de asynchrone methode wordt hervat, en de wachtende bewerking met succes is voltooid en een Task<TResult> was, dan wordt de TResult geretourneerd. Als de Task of Task<TResult> die werd afgewacht, in de Canceled status beëindigd is, wordt er een OperationCanceledException uitzondering opgeworpen. Als de Task of Task<TResult> waarop werd gewacht en die eindigde in de Faulted toestand, wordt de uitzondering die de fout veroorzaakte opgeworpen. Een Task fout kan optreden als gevolg van meerdere uitzonderingen, maar er wordt slechts één van deze uitzonderingen doorgegeven. De Task.Exception eigenschap retourneert echter een AggregateException uitzondering die alle fouten bevat.

Als een synchronisatiecontext (SynchronizationContext object) is gekoppeld aan de thread die de asynchrone methode uitvoerde op het moment van opschorten (bijvoorbeeld als de SynchronizationContext.Current eigenschap niet nullis), wordt de asynchrone methode hervat op dezelfde synchronisatiecontext met behulp van de methode van Post de context. Anders is het afhankelijk van de taakplanner (TaskScheduler object) die op het moment van schorsing actueel was. Dit is doorgaans de standaardtaakplanner (TaskScheduler.Default), die is gericht op de threadpool. Deze taakplanner bepaalt of de verwachte asynchrone bewerking moet worden hervat waar deze is voltooid of of de hervatting moet worden gepland. Met de standaardplanner kan de voortzetting doorgaans worden uitgevoerd op de thread waar de afgewachte operatie is voltooid.

Wanneer een asynchrone methode wordt aangeroepen, wordt de hoofdtekst van de functie synchroon uitgevoerd totdat de eerste wachtexpressie wordt uitgevoerd op een te wachten exemplaar dat nog niet is voltooid, waarna de aanroep terugkeert naar de aanroeper. Als de asynchrone methode niet retourneert void, wordt een Task of Task<TResult> object geretourneerd om de lopende berekening weer te geven. Als in een niet-void asynchrone methode een return-opdracht wordt aangetroffen of het einde van de methodebody wordt bereikt, wordt de taak voltooid in de RanToCompletion uiteindelijke toestand. Als een niet-verwerkte uitzondering ervoor zorgt dat de besturing de hoofdtekst van de asynchrone methode verlaat, eindigt de taak in de Faulted toestand. Als deze uitzondering een OperationCanceledExceptionuitzondering is, eindigt de taak in plaats daarvan in de Canceled status. Op deze manier wordt het resultaat of de uitzondering uiteindelijk gepubliceerd.

Er zijn verschillende belangrijke variaties van dit gedrag. Om prestatie redenen, als een taak al is voltooid op het moment dat de taak wordt verwacht, wordt de controle niet geretourneerd en gaat de functie door met uitvoeren. Daarnaast is terugkeren naar de oorspronkelijke context niet altijd het gewenste gedrag en kan worden gewijzigd; dit wordt in de volgende sectie gedetailleerder beschreven.

Configureren van onderbreking en hervatting met Yield en ConfigureAwait

Verschillende methoden bieden meer controle over de uitvoering van een asynchrone methode. U kunt bijvoorbeeld de Task.Yield methode gebruiken om een rendementspunt te introduceren in de asynchrone methode:

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

Dit komt overeen met het asynchroon terugplaatsen of plannen in de huidige context.

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

U kunt de Task.ConfigureAwait methode ook gebruiken voor betere controle over schorsing en hervatting in een asynchrone methode. Zoals eerder vermeld, wordt de huidige context standaard vastgelegd op het moment dat een asynchrone methode wordt onderbroken en die vastgelegde context wordt gebruikt om de voortzetting van de asynchrone methode aan te roepen bij hervatting. In veel gevallen is dit het exacte gedrag dat u wilt. In andere gevallen geeft u mogelijk niet om de vervolgcontext en kunt u betere prestaties bereiken door dergelijke berichten terug te zetten naar de oorspronkelijke context. Als u dit wilt inschakelen, gebruikt u de Task.ConfigureAwait-methode om de wachtbewerking geen context te laten vastleggen en hervatten, maar om door te gaan met de uitvoering overal waar de asynchrone bewerking die werd afgewacht is voltooid.

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Een asynchrone bewerking annuleren

Vanaf .NET Framework 4 bieden TAP-methoden die ondersteuning bieden voor annulering ten minste één overbelasting die een annuleringstoken (CancellationToken object) accepteert.

Er wordt een annuleringstoken gemaakt via een annuleringstokenbron (CancellationTokenSource object). De eigenschap Token van de bron retourneert het annuleringstoken dat wordt gesignaleerd wanneer de methode Cancel van de bron wordt aangeroepen. Als u bijvoorbeeld één webpagina wilt downloaden en u de bewerking wilt annuleren, maakt u een CancellationTokenSource object, geeft u het token door aan de TAP-methode en roept u de methode van Cancel de bron aan wanneer u klaar bent om de bewerking te annuleren:

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

Als u meerdere asynchrone aanroepen wilt annuleren, kunt u hetzelfde token doorgeven aan alle aanroepen:

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

U kunt hetzelfde token ook doorgeven aan een selectieve subset van bewerkingen:

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

Belangrijk

Annuleringsaanvragen kunnen worden gestart vanuit een thread.

U kunt de CancellationToken.None waarde doorgeven aan elke methode die een annuleringstoken accepteert om aan te geven dat annulering nooit wordt aangevraagd. Hierdoor retourneert de CancellationToken.CanBeCanceled-eigenschap false, en kan de aangeroepen methode dienovereenkomstig worden geoptimaliseerd. Voor testdoeleinden kunt u ook een vooraf geannuleerd annuleringstoken doorgeven dat wordt geïnstantieerd met behulp van de constructor die een Boole-waarde accepteert om aan te geven of het token moet beginnen met een al geannuleerde of niet-geannuleerde status.

Deze benadering van annulering heeft verschillende voordelen:

  • U kunt hetzelfde annuleringstoken doorgeven aan een willekeurig aantal asynchrone en synchrone bewerkingen.

  • Hetzelfde annuleringsverzoek kan worden verspreid naar een willekeurig aantal listeners.

  • De ontwikkelaar van de asynchrone API heeft volledige controle over of annulering kan worden aangevraagd en wanneer deze van kracht kan worden.

  • De code die de API verbruikt, kan selectief bepalen welke asynchrone aanroepen worden doorgegeven aan annuleringsaanvragen.

Voortgang bewaken

Sommige asynchrone methoden maken voortgang zichtbaar via een voortgangsinterface die is doorgegeven aan de asynchrone methode. Denk bijvoorbeeld aan een functie waarmee asynchroon een tekenreeks met tekst wordt gedownload. Daarnaast worden voortgangsupdates gegenereerd die het percentage van de download bevatten dat tot nu toe is voltooid. Een dergelijke methode kan als volgt worden gebruikt in een WPF-toepassing (Windows Presentation Foundation):

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

Gebruik maken van de ingebouwde taakgebaseerde combinatoren

De System.Threading.Tasks naamruimte bevat verschillende methoden voor het opstellen en werken met taken.

Task.Run

De Task klasse bevat verschillende Run methoden waarmee u eenvoudig werk kunt overdragen als een Task of Task<TResult> naar de thread-pool, bijvoorbeeld:

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

Sommige van deze Run methoden, zoals de Task.Run(Func<Task>) overbelasting, bestaan als afkorting voor de TaskFactory.StartNew methode. Met deze overload kunt u 'await' gebruiken binnen het uitgelagerde werk, bijvoorbeeld:

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

Dergelijke overbelastingen zijn logisch gelijk aan het gebruik van de TaskFactory.StartNew methode in combinatie met de Unwrap extensiemethode in de taakparallelbibliotheek.

Task.FromResult

Gebruik de FromResult methode in scenario's waarin gegevens wellicht al beschikbaar zijn en alleen moeten worden teruggegeven vanuit een taakretournerende methode die in een Task<TResult> is verheven.

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

Gebruik de WhenAll methode om asynchroon te wachten op meerdere asynchrone bewerkingen die worden weergegeven als taken. De methode heeft meerdere overloads die ondersteuning bieden voor een set niet-generieke taken of een niet-uniforme set generieke taken (bijvoorbeeld asynchroon wachten op meerdere void-voltooide operaties, of asynchroon wachten op meerdere methoden voor het retourneren van waarden waarbij elke waarde een ander type kan hebben) en om een uniforme set generieke taken te ondersteunen (zoals asynchroon wachten op meerdere TResult-returnerende methoden).

Stel dat u e-mailberichten naar verschillende klanten wilt verzenden. U kunt het verzenden van de berichten overlappen, zodat u niet wacht tot één bericht is voltooid voordat u het volgende verzendt. U kunt ook achterhalen wanneer de verzendbewerkingen zijn voltooid en of er fouten zijn opgetreden:

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

Met deze code worden uitzonderingen die kunnen optreden niet expliciet verwerkt, maar worden ze doorgegeven vanuit await naar de resulterende taak van WhenAll. Als u de uitzonderingen wilt afhandelen, kunt u code zoals de volgende gebruiken:

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

In dit geval, als een asynchrone bewerking mislukt, worden alle uitzonderingen samengevoegd in een AggregateException uitzondering, die wordt opgeslagen in de Task die wordt geretourneerd door de WhenAll methode. Er wordt echter slechts één van deze uitzonderingen doorgegeven door het await trefwoord. Als u alle uitzonderingen wilt onderzoeken, kunt u de vorige code als volgt herschrijven:

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

Laten we eens kijken naar een voorbeeld van het downloaden van meerdere bestanden van het web asynchroon. In dit geval hebben alle asynchrone bewerkingen homogene resultaattypen en is het eenvoudig om toegang te krijgen tot de resultaten:

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

U kunt dezelfde technieken voor het afhandelen van uitzonderingen gebruiken die we in het vorige void-terugkerend scenario hebben besproken.

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

U kunt de WhenAny methode gebruiken om asynchroon te wachten op slechts één van meerdere asynchrone bewerkingen die worden weergegeven als taken die moeten worden voltooid. Deze methode dient voor vier primaire use cases:

  • Redundantie: Het uitvoeren van een bewerking meerdere keren en het uitvoeren van de bewerking die het eerst wordt voltooid (bijvoorbeeld contact opnemen met meerdere webservices voor aandelenkoersen die één resultaat opleveren en het resultaat selecteren dat het snelst wordt voltooid).

  • Interleaving: Meerdere bewerkingen gelijktijdig starten en wachten tot ze allemaal voltooid zijn, terwijl ze worden verwerkt zodra ze klaar zijn.

  • Snelheidsbeperking: aanvullende operaties toestaan om te beginnen als anderen zijn voltooid. Dit is een uitbreiding van het interleavingsscenario.

  • Vroege borgtocht: Een bewerking die wordt vertegenwoordigd door taak t1, kan bijvoorbeeld worden gegroepeerd in een WhenAny taak met een andere taak t2 en u kunt wachten op de WhenAny taak. Taak t2 kan een time-out of annulering vertegenwoordigen, of een ander signaal dat ervoor zorgt dat de WhenAny taak wordt voltooid voordat t1 is voltooid.

Redundantie

Overweeg een geval waarin u een beslissing wilt nemen over het kopen van een aandelen. Er zijn verschillende stockaanbevelingswebservices die u vertrouwt, maar afhankelijk van de dagelijkse belasting kan elke service op verschillende momenten traag zijn. U kunt de WhenAny methode gebruiken om een melding te ontvangen wanneer een bewerking is voltooid:

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

In tegenstelling tot WhenAll, dat de uitgepakte resultaten van alle taken die met succes zijn voltooid retourneert, retourneert WhenAny de taak die is voltooid. Als een taak mislukt, is het belangrijk om te weten dat deze is mislukt en als een taak slaagt, is het belangrijk om te weten met welke taak de retourwaarde is gekoppeld. Daarom moet u het resultaat van de geretourneerde taak openen of deze verder wachten, zoals in dit voorbeeld wordt weergegeven.

Net als bij WhenAll, moet u in staat zijn om uitzonderingen aan te kunnen. Omdat u de voltooide taak terug ontvangt, kunt u wachten op de geretourneerde taak om fouten door te geven en try/catch deze op de juiste manier worden doorgegeven, bijvoorbeeld:

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

Bovendien kunnen de volgende taken mislukken, zelfs als een eerste taak is voltooid. Op dit moment hebt u verschillende opties voor het verwerken van uitzonderingen: u kunt wachten totdat alle gestarte taken zijn voltooid, in welk geval u de WhenAll methode kunt gebruiken of u kunt besluiten dat alle uitzonderingen belangrijk zijn en moeten worden geregistreerd. Hiervoor kunt u vervolgen gebruiken om een melding te ontvangen wanneer taken asynchroon zijn voltooid:

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

of:

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

of zelfs:

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

Ten slotte kunt u alle resterende bewerkingen annuleren:

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

Versnijden

Overweeg een geval waarin u afbeeldingen downloadt van het web en elke afbeelding verwerkt (bijvoorbeeld het toevoegen van de afbeelding aan een ui-besturingselement). U verwerkt de afbeeldingen sequentieel op de UI-thread, maar u wilt de afbeeldingen zo gelijktijdig mogelijk downloaden. U wilt ook niet wachten met het toevoegen van de afbeeldingen aan de user interface totdat ze allemaal zijn gedownload. In plaats daarvan wilt u ze toevoegen zodra ze zijn voltooid.

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

U kunt ook interleaving toepassen op een scenario waarbij rekenintensieve verwerking ThreadPool van de gedownloade afbeeldingen is betrokken, bijvoorbeeld:

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

Snelheidsbeperking

Bekijk het interleavingsvoorbeeld, behalve dat de gebruiker zoveel afbeeldingen downloadt dat de downloads moeten worden beperkt; bijvoorbeeld, u wilt dat slechts een bepaald aantal downloads gelijktijdig plaatsvindt. Hiervoor kunt u een subset van de asynchrone bewerkingen starten. Wanneer de bewerkingen zijn voltooid, kunt u extra bewerkingen starten om deze te vervangen.

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

Vroege borgtocht

Houd er rekening mee dat u asynchroon wacht totdat een bewerking is voltooid terwijl u tegelijkertijd reageert op de annuleringsaanvraag van een gebruiker (de gebruiker heeft bijvoorbeeld op een knop Annuleren geklikt). De volgende code illustreert dit scenario:

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

Met deze implementatie wordt de gebruikersinterface opnieuw ingeschakeld zodra u besluit om te stoppen, maar worden de onderliggende asynchrone bewerkingen niet geannuleerd. Een ander alternatief is het annuleren van de in behandeling zijnde bewerkingen wanneer u besluit om af te haken, maar de gebruikersinterface pas opnieuw opzetten nadat de bewerkingen zijn voltooid, waarbij ze mogelijk vroegtijdig eindigen als gevolg van de annuleringsaanvraag.

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

Een ander voorbeeld van vroege borgtocht omvat het gebruik van de WhenAny methode in combinatie met de Delay methode, zoals besproken in de volgende sectie.

Taakvertraging

U kunt de Task.Delay methode gebruiken om pauzes te introduceren in de uitvoering van een asynchrone methode. Dit is handig voor veel soorten functionaliteit, waaronder het bouwen van polling-lussen en het vertragen van de verwerking van gebruikersinvoer gedurende een vooraf bepaalde periode. De Task.Delay methode kan ook handig zijn in combinatie met Task.WhenAny bij het implementeren van time-outs op awaits.

Als een taak die deel uitmaakt van een grotere asynchrone bewerking (bijvoorbeeld een ASP.NET-webservice) te lang duurt, kan de algehele bewerking lijden, met name als deze niet kan worden voltooid. Daarom is het belangrijk dat u een time-out kunt instellen bij het wachten op een asynchrone bewerking. De synchrone Task.Wait, Task.WaitAll, en Task.WaitAny methoden accepteren time-outwaarden, maar de bijbehorende TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny en de eerder genoemde Task.WhenAll/Task.WhenAny methoden niet. U kunt in plaats daarvan een time-out implementeren door Task.Delay en Task.WhenAny te combineren.

Stel dat u in uw UI-toepassing een afbeelding wilt downloaden en de gebruikersinterface wilt deactiveren terwijl de afbeelding wordt gedownload. Als het downloaden echter te lang duurt, wilt u de gebruikersinterface opnieuw inschakelen en de download negeren:

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

Hetzelfde geldt voor meerdere downloads, omdat WhenAll een taak wordt geretourneerd:

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

Op taken gebaseerde combinaties bouwen

Omdat een taak een asynchrone bewerking volledig kan vertegenwoordigen en synchrone en asynchrone mogelijkheden biedt voor het samenvoegen met de bewerking, het ophalen van de resultaten, enzovoort, kunt u nuttige bibliotheken bouwen van combinaties die taken samenstellen om grotere patronen te bouwen. Zoals besproken in de vorige sectie, bevat .NET verschillende ingebouwde combinaties, maar u kunt ook uw eigen combinaties maken. De volgende secties bevatten verschillende voorbeelden van mogelijke combinatiemethoden en -typen.

Opnieuw Proberen Bij Fout

In veel situaties kunt u een bewerking opnieuw proberen als een eerdere poging mislukt. Voor synchrone code kunt u een helpermethode maken, zoals RetryOnFault in het volgende voorbeeld om dit te bereiken:

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

U kunt een bijna identieke helpermethode bouwen voor asynchrone bewerkingen die zijn geïmplementeerd met TAP en dus taken retourneren:

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

Vervolgens kunt u deze combinatiefunctie gebruiken om nieuwe pogingen te coderen in de logica van de toepassing; bijvoorbeeld:

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

U kunt de RetryOnFault functie verder uitbreiden. De functie kan bijvoorbeeld een andere Func<Task> functie accepteren die wordt aangeroepen tussen nieuwe pogingen om te bepalen wanneer de bewerking opnieuw moet worden uitgevoerd, bijvoorbeeld:

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

Vervolgens kunt u de functie als volgt gebruiken om een seconde te wachten voordat u de bewerking opnieuw probeert uit te voeren:

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

Soms kunt u profiteren van redundantie om de latentie en kansen voor succes van een bewerking te verbeteren. Overweeg meerdere webservices die aandelenkoersen bieden, maar op verschillende tijdstippen van de dag kunnen elke service verschillende niveaus van kwaliteit en reactietijden bieden. Als u deze schommelingen wilt afhandelen, kunt u aanvragen verzenden naar alle webservices en zodra u een reactie van een van de services krijgt, annuleert u de resterende aanvragen. U kunt een helperfunctie implementeren om het eenvoudiger te maken om dit algemene patroon van het starten van meerdere bewerkingen te implementeren, te wachten op een en vervolgens de rest te annuleren. De NeedOnlyOne functie in het volgende voorbeeld illustreert dit scenario:

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

U kunt deze functie vervolgens als volgt gebruiken:

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

Afwisselende Operaties

Er is een potentieel prestatieprobleem bij het gebruik van de WhenAny-methode om een interleaving-scenario te ondersteunen wanneer u met grote hoeveelheden taken werkt. Elke aanroep naar WhenAny resulteert in een voortzetting die bij elke taak wordt geregistreerd. Voor N aantal taken resulteert dit in O(N2) vervolgbewerkingen die zijn gemaakt gedurende de levensduur van de interleavingsbewerking. Als u met een grote set taken werkt, kunt u een combinatie (Interleaved in het volgende voorbeeld) gebruiken om het prestatieprobleem op te lossen:

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

Vervolgens kunt u de combinatie gebruiken om de resultaten van taken te verwerken wanneer ze zijn voltooid; bijvoorbeeld:

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

WhenAllOrFirstException

In bepaalde scenario's voor spreiding/gegevensverzameling wilt u mogelijk wachten op alle taken in een set, tenzij een van deze fouten optreedt. In dat geval wilt u stoppen met wachten zodra de uitzondering optreedt. U kunt dit doen met een combinatiemethode, zoals WhenAllOrFirstException in het volgende voorbeeld:

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

Op taken gebaseerde gegevensstructuren bouwen

Naast de mogelijkheid om aangepaste op taken gebaseerde combinaties te maken, met een gegevensstructuur in Task en Task<TResult> die zowel de resultaten van een asynchrone bewerking als de benodigde synchronisatie vertegenwoordigt om ermee samen te voegen, is het een krachtig type waarop aangepaste gegevensstructuren kunnen worden gebouwd die moeten worden gebruikt in asynchrone scenario's.

AsyncCache

Een belangrijk aspect van een taak is dat het kan worden uitgedeeld aan meerdere consumenten, die allemaal erop kunnen wachten, voortzettingen met de taak kunnen registreren, het resultaat of de uitzonderingen ervan kunnen verkrijgen (in het geval van Task<TResult>) enzovoort. Dit maakt Task en Task<TResult> perfect geschikt voor gebruik in een asynchrone cache-infrastructuur. Hier volgt een voorbeeld van een kleine, maar krachtige asynchrone cache die is gebouwd op 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;
        }
    }
}

De AsyncCache<TKey,TValue-klasse> accepteert als delegeren naar zijn constructor een functie die een TKey neemt en een Task<TResult> retourneert. Eerder geopende waarden uit de cache worden opgeslagen in de interne woordenlijst en zorgt AsyncCache ervoor dat er slechts één taak per sleutel wordt gegenereerd, zelfs als de cache gelijktijdig wordt geopend.

U kunt bijvoorbeeld een cache bouwen voor gedownloade webpagina's:

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

U kunt deze cache vervolgens gebruiken in asynchrone methoden wanneer u de inhoud van een webpagina nodig hebt. De AsyncCache klasse zorgt ervoor dat u zo weinig mogelijk pagina's downloadt en de resultaten in de cache opgeslagen.

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

U kunt ook taken gebruiken om gegevensstructuren te bouwen voor het coördineren van asynchrone activiteiten. Overweeg een van de klassieke parallelle ontwerppatronen: producent/consument. In dit patroon genereren producenten gegevens die door consumenten worden gebruikt en kunnen de producenten en consumenten parallel worden gerund. De consument verwerkt bijvoorbeeld item 1, dat eerder is gegenereerd door een producent die nu item 2 produceert. Voor het patroon producent/consument hebt u altijd een gegevensstructuur nodig om het werk op te slaan dat door producenten is gemaakt, zodat de consumenten op de hoogte kunnen worden gesteld van nieuwe gegevens en deze kunnen vinden wanneer ze beschikbaar zijn.

Hier volgt een eenvoudige gegevensstructuur, gebouwd op basis van taken, waarmee asynchrone methoden kunnen worden gebruikt als producenten en consumenten:

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

Nu deze gegevensstructuur is ingesteld, kunt u code schrijven, zoals de volgende:

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

De System.Threading.Tasks.Dataflow naamruimte bevat het BufferBlock<T> type dat u op een vergelijkbare manier kunt gebruiken, maar zonder dat u een aangepast verzamelingstype hoeft te maken:

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

Opmerking

De System.Threading.Tasks.Dataflow naamruimte is beschikbaar als een NuGet-pakket. Als u de assembly wilt installeren die de System.Threading.Tasks.Dataflow naamruimte bevat, opent u uw project in Visual Studio, kiest u NuGet-pakketten beheren in het menu Project en zoekt u online naar het System.Threading.Tasks.Dataflow pakket.

Zie ook