Share via


Het Asynchrone patroon op basis van taken 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. Op taal gebaseerde asynchrone ondersteuning verbergt callbacks door asynchrone bewerkingen te wachten binnen de normale controlestroom en door compiler gegenereerde code biedt dezelfde ondersteuning op API-niveau.

Uitvoering onderbreken 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.)

Onder de dekking installeert de wachtfunctionaliteit een callback voor de taak met behulp van een vervolg. Deze callback hervat de asynchrone methode op het moment van ophanging. Wanneer de asynchrone methode wordt hervat, wordt de asynchrone methode geretourneerd als de wachtende bewerking is voltooid en een Task<TResult>, TResult wordt het geretourneerd. Als de Task of Task<TResult> die in afwachting van de Canceled status is beëindigd, wordt er een OperationCanceledException uitzondering gegenereerd. Als de Task of Task<TResult> die in afwachting was van de Faulted status beëindigd, wordt de uitzondering die ervoor zorgde dat deze fout optreedt, gegenereerd. 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 die de voltooide bewerking heeft 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-ongeldige asynchrone methode een retourinstructie wordt aangetroffen of het einde van de hoofdtekst van de methode wordt bereikt, wordt de taak voltooid in de RanToCompletion uiteindelijke status. Als een niet-verwerkte uitzondering ervoor zorgt dat het besturingselement de hoofdtekst van de asynchrone methode verlaat, eindigt de taak in de Faulted status. 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. Als een taak al is voltooid op het moment dat de taak wordt verwacht, wordt het besturingselement niet geretourneerd en blijft de functie worden uitgevoerd. Daarnaast is terugkeren naar de oorspronkelijke context niet altijd het gewenste gedrag en kan worden gewijzigd; dit wordt in de volgende sectie gedetailleerder beschreven.

Ophanging en hervatting configureren met Opbrengst 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 asynchroon posten of plannen naar 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 te informeren dat deze niet kan worden vastgelegd en hervat in de context, maar om door te gaan met de uitvoering waar de asynchrone bewerking 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 van Token de bron retourneert het annuleringstoken dat wordt aangegeven wanneer de methode van Cancel 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 wordt de CancellationToken.CanBeCanceled eigenschap geretourneerd falseen 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; }
}

De ingebouwde combinaties op basis van taken gebruiken

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 kunt offloaden als een Task of Task<TResult> naar de threadpool, 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 overbelasting kunt u wachten binnen het offloaded 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 mogelijk al beschikbaar zijn en alleen moeten worden geretourneerd vanuit een methode voor het retourneren van taken die in een 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

Gebruik de WhenAll methode om asynchroon te wachten op meerdere asynchrone bewerkingen die worden weergegeven als taken. De methode heeft meerdere overbelastingen die ondersteuning bieden voor een set niet-algemene taken of een niet-uniforme set algemene taken (bijvoorbeeld asynchroon wachten op meerdere niet-geretourneerde bewerkingen, of asynchroon wachten op meerdere methoden voor het retourneren van waarden waarbij elke waarde een ander type kan hebben) en ter ondersteuning van een uniforme set algemene taken (zoals asynchroon wachten op meerdere TResultmethoden voor het retourneren van waarden).

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 kunnen uitzonderingen worden await doorgegeven vanuit de resulterende taak 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 methode die wordt geretourneerd.WhenAll 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 scenario voor het retourneren van ongeldigheid 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 starten en wachten totdat alle bewerkingen zijn voltooid, maar ze worden verwerkt wanneer ze zijn voltooid.

  • Beperking: aanvullende bewerkingen 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, waardoor de niet-uitgepakte resultaten worden geretourneerd van alle taken die zijn voltooid, WhenAny wordt de taak geretourneerd 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);

Interleaving

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 de installatiekopieën ook niet toevoegen aan de gebruikersinterface 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{}
}

Beperking

Bekijk het interleavingsvoorbeeld, behalve dat de gebruiker zoveel afbeeldingen downloadt die de downloads moeten worden beperkt; U wilt bijvoorbeeld dat er 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 hun plaats te vinden:

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 betalen, maar annuleert u de onderliggende asynchrone bewerkingen niet. Een ander alternatief is het annuleren van de in behandeling zijnde bewerkingen wanneer u besluit om de gebruikersinterface te redden, maar niet opnieuw totdat de bewerkingen zijn voltooid, mogelijk als gevolg van een vroegtijdige beëindiging vanwege 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.

Task.Delay

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 het implementeren van time-outs op wachttijden.

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 uitvoeren bij het wachten op een asynchrone bewerking. De synchrone , en methoden accepteren time-outwaarden, maar de bijbehorendeTaskFactory.ContinueWhenAny/TaskFactory.ContinueWhenAll en de eerder genoemde Task.WhenAllTask.WhenAny/methoden niet.Task.WaitAnyTask.WaitAllTask.Wait In plaats daarvan kunt u een time-out implementeren Task.Delay en Task.WhenAny combineren.

Stel dat u in uw UI-toepassing een installatiekopieën wilt downloaden en de gebruikersinterface wilt uitschakelen terwijl de installatiekopieën worden 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 proberenOnFault

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

Interleaved Operations

Er is een mogelijk prestatieprobleem met het gebruik van de WhenAny methode om een interleaving-scenario te ondersteunen wanneer u met grote sets taken werkt. Elke aanroep om te 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 gemachtigde aan de constructor van de bijbehorende constructor een functie die een TKey en retourneert een Task<TResult>. 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 verbruikt en kunnen de producenten en consumenten parallel worden uitgevoerd. 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);
}

Notitie

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