Utilisation du modèle asynchrone basé sur les tâches

Lorsque vous utilisez le modèle asynchrone basé sur des tâches (TAP) pour travailler avec des opérations asynchrones, vous pouvez utiliser des rappels pour attendre sans bloquer. Pour les tâches, ce modèle utilise des méthodes telles que Task.ContinueWith. La prise en charge asynchrone basée sur le langage masque les rappels en permettant aux opérations asynchrones d’être attendues dans le flux de contrôle normal, et le code généré par le compilateur fournit cette même prise en charge au niveau de l’API.

Suspension de l'exécution en utilisant Await

Vous pouvez utiliser le mot clé Await en C# et l’opérateur Await en Visual Basic pour attendre Task et Task<TResult> objets de manière asynchrone. Lorsque vous attendez un Task, l’expression await est de type void. Lorsque vous attendez un Task<TResult>, l’expression await est de type TResult. Une await expression doit apparaître à l’intérieur du corps d’une méthode asynchrone. (Ces fonctionnalités de langage ont été introduites dans .NET Framework 4.5.)

En arrière-plan, la fonctionnalité await installe un rappel sur la tâche à l’aide d’une continuation. Ce rappel reprend la méthode asynchrone au point d’interruption. Quand la méthode asynchrone est reprise, si l’opération attendue s’est terminée correctement et est une Task<TResult>, son TResult est retourné. Si la Task ou Task<TResult> attendue s’est terminée dans l’état Canceled, une exception OperationCanceledException est levée. Si la Task ou Task<TResult> attendue s’est terminée dans l’état Faulted, l’exception qui a entraîné l’erreur est levée. Une Task peut échouer à cause de plusieurs exceptions, mais seule une de ces exceptions est propagée. Toutefois, la Task.Exception propriété retourne une AggregateException exception qui contient toutes les erreurs.

Si un contexte de synchronisation (SynchronizationContext objet) est associé au thread qui exécutait la méthode asynchrone au moment de la suspension (par exemple, si la SynchronizationContext.Current propriété n’est pas null), la méthode asynchrone reprend sur ce même contexte de synchronisation à l’aide de la méthode du Post contexte. Dans le cas contraire, il s’appuie sur le planificateur de tâches (TaskScheduler objet) qui était en cours au moment de la suspension. En règle générale, il s’agit du planificateur de tâches par défaut (TaskScheduler.Default), qui cible le pool de threads. Ce planificateur de tâches détermine si l’opération asynchrone attendue doit reprendre là où elle s'est terminée ou si la reprise doit être planifiée. Le planificateur par défaut permet généralement à la continuation de s’exécuter sur le thread qui a effectué l’opération attendue.

Lorsque vous appelez une méthode asynchrone, elle exécute de façon synchrone le corps de la fonction jusqu'à la première expression await sur une instance awaitable qui n'est pas encore terminée, moment où l'appel retourne à l'appelant. Si la méthode asynchrone ne retourne pas void, elle retourne soit un Task soit un Task<TResult> pour représenter le calcul en cours. Dans une méthode asynchrone non void, si une instruction return est rencontrée ou que la fin du corps de la méthode est atteinte, la tâche est terminée dans l’état RanToCompletion final. Si une exception non gérée cause la sortie du contrôle du corps de la méthode asynchrone, la tâche se termine dans l’état Faulted. Si cette exception est un OperationCanceledException, la tâche se termine à la place dans l’état Canceled . De cette façon, le résultat ou l’exception est finalement publié.

Plusieurs variantes importantes de ce comportement existent. Pour des raisons de performances, si une tâche est déjà terminée à ce moment-là où elle est attendue, le contrôle n’est pas cédé et la fonction continue de s’exécuter. En outre, le retour au contexte d’origine n’est pas toujours le comportement souhaité et peut être modifié ; ce comportement est décrit plus en détail dans la section suivante.

Configuration de la suspension et de la reprise avec Yield et ConfigureAwait

Plusieurs méthodes fournissent davantage de contrôle sur l’exécution d’une méthode asynchrone. Par exemple, vous pouvez utiliser la Task.Yield méthode pour introduire un point de rendement dans la méthode asynchrone :

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

Cette méthode est équivalente à une publication ou planification asynchrone pour revenir au contexte actuel.

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

Vous pouvez également utiliser la Task.ConfigureAwait méthode pour mieux contrôler la suspension et la reprise dans une méthode asynchrone. Comme mentionné précédemment, par défaut, le contexte actuel est capturé au moment où une méthode asynchrone est suspendue, et ce contexte capturé est utilisé pour appeler la continuation de la méthode asynchrone lors de la reprise. Dans de nombreux cas, il s’agit du comportement exact souhaité. Dans d'autres cas, vous ne vous souciez peut-être pas du contexte de continuation, et vous pouvez obtenir de meilleures performances en évitant de revenir dans le contexte d'origine. Pour activer ce comportement, utilisez la méthode Task.ConfigureAwait pour indiquer à l'opération await de ne pas capturer ni reprendre sur le contexte, mais de continuer l'exécution là où l'opération asynchrone en cours d'attente s'est terminée :

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Awaitables, ConfigureAwait et SynchronizationContext

await fonctionne avec n’importe quel type qui satisfait le modèle d’expression attendu, pas seulement Task. Un type est awaitable s'il fournit une méthode compatible GetAwaiter qui retourne un type avec les membres IsCompleted, OnCompleted et GetResult. Dans la plupart des API publiques, retour Task, Task<TResult>, ValueTask, ou ValueTask<TResult>. Utilisez des awaitables personnalisés uniquement pour les scénarios spécialisés.

Utilisez ConfigureAwait quand la continuation n’a pas besoin du contexte de l’appelant. Dans le code d’application qui met à jour une interface utilisateur, la capture de contexte est souvent nécessaire. Dans le code de bibliothèque réutilisable, il est généralement préférable d'utiliser ConfigureAwait(false), car il évite les sauts de contexte inutiles et réduit le risque de blocage pour les utilisateurs qui bloquent.

ConfigureAwait(false) modifie la planification de continuation, et non pas ExecutionContext le flux. Pour obtenir une explication plus approfondie du comportement du contexte, consultez ExecutionContext et SynchronizationContext.

Annulation d’une opération asynchrone

À compter de .NET Framework 4, les méthodes TAP qui prennent en charge l’annulation fournissent au moins une surcharge qui accepte un jeton d’annulation (CancellationToken objet).

Vous créez un jeton d’annulation via une source de jeton d’annulation (CancellationTokenSource objet). La propriété Token de la source retourne le jeton d'annulation qui signale quand la méthode Cancel de la source est appelée.

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

Par exemple, si vous souhaitez télécharger une page web unique et que vous souhaitez être en mesure d’annuler l’opération, créez un CancellationTokenSource objet, transmettez son jeton à la méthode TAP, puis appelez la méthode de Cancel la source lorsque vous êtes prêt à annuler l’opération :

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

Vous pouvez également passer le même jeton à un sous-ensemble sélectif d’opérations :

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

Importante

Tout thread peut lancer des demandes d’annulation.

Vous pouvez transmettre la CancellationToken.None valeur à n’importe quelle méthode qui accepte un jeton d’annulation pour indiquer que l’annulation n’est jamais demandée. Cette valeur entraîne la propriété CancellationToken.CanBeCanceled à retourner false, et la méthode appelée peut donc être optimisée en conséquence. À des fins de test, vous pouvez également transmettre un jeton d’annulation pré-annulé instancié à l’aide du constructeur qui accepte une valeur booléenne pour indiquer si le jeton doit démarrer dans un état déjà annulé ou non annulable.

Cette approche de l’annulation présente plusieurs avantages :

  • Vous pouvez transmettre le même jeton d’annulation à n’importe quel nombre d’opérations asynchrones et synchrones.

  • La même demande d’annulation peut être envoyée à un nombre quelconque d'auditeurs.

  • Le développeur de l’API asynchrone contrôle complètement si l’annulation peut être demandée et quand elle prend effet.

  • Le code qui consomme l’API peut déterminer de manière sélective les appels asynchrones auxquels les demandes d’annulation sont envoyées.

Contrôle de la progression

Certaines méthodes asynchrones exposent la progression via une interface de suivi de progression que vous transmettez à la méthode asynchrone. Par exemple, considérez une fonction qui télécharge de façon asynchrone une chaîne de texte, et le long de la route déclenche des mises à jour de progression qui incluent le pourcentage du téléchargement terminé jusqu’à présent. Vous pouvez utiliser une telle méthode dans une application Windows Presentation Foundation (WPF) comme suit :

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

Utilisation des combinateurs intégrés basés sur des tâches

L’espace de noms System.Threading.Tasks inclut plusieurs méthodes de composition et d’utilisation des tâches.

Remarque

Plusieurs exemples de code de cette section utilisent Bitmap, ce qui nécessite le package System.Drawing.Common et est pris en charge uniquement sur Windows. Les modèles asynchrones qu’ils illustrent s’appliquent à toutes les plateformes ; remplacez une bibliothèque d’imagerie multiplateforme pour les cibles non Windows.

Task.Run

La classe Task comprend plusieurs méthodes Run qui vous permettent de décharger facilement le travail, en tant que Task ou Task<TResult>, vers le pool de threads. Par exemple:

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

Certaines de ces Run méthodes, telles que la Task.Run(Func<Task>) surcharge, existent comme raccourcis pour la TaskFactory.StartNew méthode. Cette surcharge vous permet d’utiliser await dans le travail déchargé. Par exemple:

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

Ces surcharges sont logiquement équivalentes à l’utilisation de la méthode TaskFactory.StartNew conjointement avec la méthode d’extension Unwrap dans la bibliothèque de tâches parallèles.

Task.FromResult

Utilisez la FromResult méthode dans les scénarios où les données peuvent déjà être disponibles et vous devez simplement les renvoyer à partir d’une méthode de retour de tâches intégrée dans un Task<TResult>:

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

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

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

Task.WhenAll

Utilisez la WhenAll méthode pour attendre de façon asynchrone plusieurs opérations asynchrones représentées en tant que tâches. La méthode comporte plusieurs surcharges qui prennent en charge un ensemble de tâches non génériques ou un ensemble non uniforme de tâches génériques (par exemple, en attente asynchrone de plusieurs opérations retournant void, ou en attente asynchrone de plusieurs méthodes retournant des valeurs où chaque valeur peut avoir un type différent) pour prendre en charge un ensemble uniforme de tâches génériques (par exemple, en attendant de manière asynchrone plusieurs méthodes retournant des valeurs de type TResult).

Supposons que vous souhaitez envoyer des messages électroniques à plusieurs clients. Vous pouvez chevaucher l’envoi des messages afin que vous n’attendiez pas qu’un message se termine avant d’envoyer le suivant. Vous pouvez également déterminer quand les opérations d’envoi se terminent et si des erreurs se produisent :

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

Ce code ne gère pas explicitement les exceptions qui peuvent se produire, mais il permet aux exceptions de se propager à partir du await sur la tâche résultante de WhenAll. Pour gérer les exceptions, utilisez du code comme suit :

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

Dans ce cas, si une opération asynchrone échoue, toutes les exceptions sont consolidées dans une AggregateException exception, qui est stockée dans l’élément Task retourné par la WhenAll méthode. Toutefois, une seule de ces exceptions est propagée par le await mot clé. Si vous souhaitez examiner toutes les exceptions, vous pouvez réécrire le code précédent comme suit :

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

Prenons un exemple de téléchargement asynchrone de plusieurs fichiers à partir du web. Dans ce cas, toutes les opérations asynchrones ont des types de résultats homogènes et il est facile d’accéder aux résultats :

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

Vous pouvez utiliser les mêmes techniques de gestion des exceptions décrites dans le scénario précédent de retour vide :

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

Task.WhenAny

Utilisez la méthode WhenAny pour attendre de façon asynchrone qu'une seule parmi plusieurs opérations asynchrones représentées sous forme de tâches se termine. Cette méthode sert quatre cas d’usage principaux :

  • Redondance : exécution d’une opération plusieurs fois et sélection de celle qui se termine en premier (par exemple, en contactant plusieurs services web de devis boursiers qui retournent un résultat unique et en sélectionnant celui qui termine le plus rapide).

  • Entrelacement : lancement de plusieurs opérations et attente de leur achèvement, mais traitement de celles-ci au fur et à mesure qu’elles se terminent.

  • Limitation : autorisation de démarrer des opérations supplémentaires une fois que d’autres sont terminées. Ce scénario est une extension du scénario d'interlacement.

  • Sauvetage anticipé : par exemple, une opération représentée par la tâche t1 peut être regroupée dans une tâche WhenAny avec une autre tâche t2, et vous pouvez attendre la tâche WhenAny. La tâche t2 peut représenter un délai d’attente ou une annulation, ou un autre signal qui entraîne la fin de la WhenAny tâche avant la fin de la tâche t1.

Redondance

Considérez un cas où vous souhaitez prendre une décision sur l’achat d’un stock. Plusieurs services web de recommandation de stock existent, mais en fonction de la charge quotidienne, chaque service peut finir par être lent à des moments différents. Utilisez la WhenAny méthode pour recevoir une notification quand une opération se termine :

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

Contrairement à WhenAll, qui retourne les résultats directs de toutes les tâches qui ont été menées à bien avec succès, WhenAny retourne la tâche qui a été complétée. Si une tâche échoue, il est important de savoir qu’elle a échoué et si une tâche réussit, il est important de savoir à quelle tâche la valeur de retour est associée. Par conséquent, vous devez accéder au résultat de la tâche retournée, ou l’attendre davantage, comme l’illustre cet exemple.

Comme avec WhenAll, vous devez être en mesure de prendre en charge les exceptions. Étant donné que vous recevez la tâche terminée, vous pouvez attendre la tâche retournée pour propager les erreurs, et utiliser try/catch dessus comme nécessaire, par exemple :

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

En outre, même si une première tâche se termine correctement, les tâches suivantes peuvent échouer. À ce stade, vous avez plusieurs options pour traiter les exceptions : vous pouvez attendre que toutes les tâches lancées soient terminées, auquel cas vous pouvez utiliser la WhenAll méthode, ou vous pouvez décider que toutes les exceptions sont importantes et doivent être journalisées. Pour ce scénario, vous pouvez utiliser des continuations pour recevoir une notification lorsque les tâches se terminent de façon asynchrone :

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

ou:

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

ou même :

private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
    foreach (var task in tasks)
    {
        try { await task; }
        catch (Exception exc) { Stubs.Log(exc); }
    }
}
Private Async Sub LogCompletionIfFailed(tasks As IEnumerable(Of Task))
    For Each task In tasks
        Try
            Await task
        Catch exc As Exception
            Stubs.Log(exc)
        End Try
    Next
End Sub

Enfin, vous pouvez annuler toutes les opérations restantes :

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

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

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

Entrelacement

Considérez un cas où vous téléchargez des images à partir du web et traitez chaque image (par exemple, en ajoutant l’image à un contrôle d’interface utilisateur). Vous traitez les images de manière séquentielle sur le thread d’interface utilisateur, mais souhaitez télécharger les images aussi simultanément que possible. En outre, vous ne souhaitez pas retarder l'ajout des images à l'interface utilisateur jusqu'à ce qu'elles soient toutes téléchargées. Au lieu de cela, vous souhaitez les ajouter au fur et à mesure qu'ils sont complétés.

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

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

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

Vous pouvez également appliquer l’entrelacement à un scénario qui implique un traitement gourmand en calcul sur les ThreadPool images téléchargées ; par exemple :

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

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

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

Limitation

Considérez l’exemple d’entrelacement, sauf que l’utilisateur télécharge tant d’images que les téléchargements doivent être limités. Par exemple, vous ne souhaitez qu’un nombre spécifique de téléchargements se produisent simultanément. Pour atteindre cet objectif, démarrez un sous-ensemble des opérations asynchrones. À mesure que les opérations se terminent, vous pouvez démarrer des opérations supplémentaires pour qu’elles prennent place :

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

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

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

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

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

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

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

Interruption anticipée

Considérez que vous attendez de façon asynchrone qu’une opération se termine tout en répondant simultanément à la demande d’annulation d’un utilisateur (par exemple, l’utilisateur a cliqué sur un bouton Annuler). Le code suivant illustre ce scénario :

class EarlyBailoutUI
{
    private CancellationTokenSource? m_cts;

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

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

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

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

Cette implémentation réactive l’interface utilisateur dès que vous décidez de procéder à un sauvetage, mais n’annule pas les opérations asynchrones sous-jacentes. Une autre alternative consisterait à annuler les opérations en attente lorsque vous décidez de procéder à un sauvetage, mais pas de rétablir l’interface utilisateur tant que les opérations ne sont pas terminées, potentiellement en raison de la fin anticipée en raison de la demande d’annulation :

class EarlyBailoutWithTokenUI
{
    private CancellationTokenSource? m_cts;

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

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

Un autre exemple de sauvetage anticipé implique l’utilisation de la WhenAny méthode conjointement avec la Delay méthode, comme indiqué dans la section suivante.

Task.Delay

Utilisez la Task.Delay méthode pour ajouter des pauses dans l’exécution d’une méthode asynchrone. Cette pause est utile pour de nombreux types de fonctionnalités, notamment la création de boucles d’interrogation et pour retarder la gestion des entrées utilisateur pendant une période prédéterminée. Vous pouvez également utiliser la méthode Task.Delay avec Task.WhenAny pour implémenter des temporisations sur les attentes.

Si une tâche qui fait partie d’une opération asynchrone plus grande (par exemple, un service web ASP.NET) prend trop de temps, l’opération globale peut souffrir, en particulier si elle ne parvient pas à se terminer. Pour cette raison, il est important de pouvoir expirer en attendant une opération asynchrone. Les méthodes synchrones Task.Wait, Task.WaitAll, et Task.WaitAny acceptent les valeurs de délai d’attente, mais les méthodes correspondantes TaskFactory.ContinueWhenAll/TaskFactory.ContinueWhenAny et les méthodes mentionnées précédemment Task.WhenAll/Task.WhenAny ne les acceptent pas. À la place, utilisez Task.Delay et Task.WhenAny ensemble pour implémenter une temporisation.

Par exemple, dans votre application d’interface utilisateur, supposons que vous souhaitez télécharger une image et désactiver l’interface utilisateur pendant le téléchargement de l’image. Toutefois, si le téléchargement prend trop de temps, vous souhaitez réactiver l’interface utilisateur et ignorer le téléchargement :

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

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

Le même principe s’applique à plusieurs téléchargements, car WhenAll retourne une tâche :

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

Génération de combinateurs basés sur des tâches

Étant donné qu’une tâche est en mesure de représenter complètement une opération asynchrone et de fournir des fonctionnalités synchrones et asynchrones pour la jointure à l’opération, la récupération de ses résultats, et ainsi de suite, vous pouvez créer des bibliothèques utiles de combinateurs qui composent des tâches pour créer des modèles plus volumineux. Comme indiqué dans la section précédente, .NET inclut plusieurs combinateurs intégrés, mais vous pouvez également créer vos propres. Les sections suivantes fournissent plusieurs exemples de méthodes et de types de combinateurs potentiels.

RetryOnFault

Dans de nombreuses situations, vous souhaitez réessayer une opération en cas d’échec d’une tentative précédente. Pour le code synchrone, vous pouvez créer une méthode d’assistance telle que RetryOnFault dans l’exemple suivant pour accomplir cette tâche :

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

Vous pouvez créer une méthode d’assistance presque identique pour les opérations asynchrones qui sont implémentées avec TAP et donc renvoyer des tâches :

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

Vous pouvez ensuite utiliser ce combinateur pour encoder les nouvelles tentatives dans la logique de l’application. Par exemple:

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

Vous pouvez étendre la RetryOnFault fonction plus loin. Par exemple, la fonction peut accepter une autre Func<Task> que la fonction appelle entre nouvelles tentatives pour déterminer quand réessayer l’opération. Par exemple:

public static async Task<T> RetryOnFaultWithDelay<T>(
    Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
    for (int i = 0; i < maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries - 1) throw; }
        await retryWhen().ConfigureAwait(false);
    }
    return default(T)!;
}
Public Async Function RetryOnFaultWithDelay(Of T)(
    func As Func(Of Task(Of T)), maxTries As Integer, retryWhen As Func(Of Task)) As Task(Of T)
    For i As Integer = 0 To maxTries - 1
        Try
            Return Await func().ConfigureAwait(False)
        Catch
            If i = maxTries - 1 Then Throw
        End Try
        Await retryWhen().ConfigureAwait(False)
    Next
    Return Nothing
End Function

Vous pouvez ensuite utiliser la fonction comme suit pour attendre une seconde avant de réessayer l’opération :

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

Parfois, vous pouvez tirer parti de la redondance pour améliorer la latence d’une opération et les chances de réussite. Considérez plusieurs services web qui fournissent des cours de bourse, mais à différentes périodes de la journée, chaque service peut fournir différents niveaux de qualité et de temps de réponse. Pour gérer ces fluctuations, vous pouvez émettre des demandes à tous les services web, et dès que vous obtenez une réponse d’un, annulez les demandes restantes. Vous pouvez implémenter une fonction d’assistance pour faciliter l’implémentation de ce modèle commun de lancement de plusieurs opérations, en attendant tout, puis en annulant le reste. La NeedOnlyOne fonction de l’exemple suivant illustre ce scénario :

public static async Task<T> NeedOnlyOne<T>(
    params Func<CancellationToken, Task<T>>[] functions)
{
    var cts = new CancellationTokenSource();
    var tasks = (from function in functions
                 select function(cts.Token)).ToArray();
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach (var task in tasks)
    {
        var ignored = task.ContinueWith(
            t => Stubs.Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return await completed;
}
Public Async Function NeedOnlyOne(Of T)(
    ParamArray functions As Func(Of CancellationToken, Task(Of T))()) As Task(Of T)
    Dim cts As New CancellationTokenSource()
    Dim tasks As Task(Of T)() = (From func In functions Select func(cts.Token)).ToArray()
    Dim completed As Task(Of T) = Await Task.WhenAny(tasks).ConfigureAwait(False)
    cts.Cancel()
    For Each task In tasks
        Dim ignored = task.ContinueWith(
            Sub(tsk) Stubs.Log(tsk), TaskContinuationOptions.OnlyOnFaulted)
    Next
    Return Await completed
End Function

Vous pouvez ensuite utiliser cette fonction comme suit :

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

Opérations entrelacées

L’utilisation de la WhenAny méthode pour prendre en charge un scénario d’entrelacement peut entraîner un problème de performances lorsque vous travaillez avec de grands ensembles de tâches. Chaque appel à WhenAny enregistre une continuation avec chaque tâche. Pour N nombre de tâches, ce processus crée des continuations O(N2) au cours de la durée de vie de l’opération d’entrelacement. Si vous utilisez un grand ensemble de tâches, utilisez un combinateur (Interleaved dans l’exemple suivant) pour résoudre le problème de performances :

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

Utilisez le combinateur pour traiter les résultats des tâches à mesure qu’elles se terminent. Par exemple:

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

WhenAllOrFirstException

Dans certains scénarios de répartition/collecte, vous voudrez peut-être attendre toutes les tâches d'un ensemble, sauf si l'une d'elles échoue. Dans ce cas, vous souhaitez arrêter d’attendre dès que l’exception se produit. Vous pouvez accomplir ce comportement à l’aide d’une méthode de combinateur comme WhenAllOrFirstException dans l’exemple suivant :

public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
    var inputs = tasks.ToList();
    var ce = new CountdownEvent(inputs.Count);
    var tcs = new TaskCompletionSource<T[]>();

    Action<Task> onCompleted = (Task completed) =>
    {
        if (completed.IsFaulted)
            tcs.TrySetException(completed.Exception!.InnerExceptions);
        if (ce.Signal() && !tcs.Task.IsCompleted)
            tcs.TrySetResult(inputs.Select(t => ((Task<T>)t).Result).ToArray());
    };

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

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

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

Création de structures de données basées sur des tâches

En plus de permettre la création de combinateurs personnalisés basés sur des tâches, avoir une structure de données dans Task et Task<TResult> qui représente à la fois les résultats d’une opération asynchrone et la synchronisation nécessaire pour s’y joindre en fait un type puissant, sur lequel il est possible de construire des structures de données personnalisées pour être utilisées dans des scénarios asynchrones.

AsyncCache

Un aspect important d’une tâche est que vous pouvez le transmettre à plusieurs consommateurs. Tous les consommateurs peuvent l'attendre, inscrire des continuations avec elle, obtenir son résultat ou ses exceptions (dans le cas de Task<TResult>), et ainsi de suite. Cet aspect rend Task et Task<TResult> convient parfaitement à l’utilisation dans une infrastructure de mise en cache asynchrone. Voici un exemple de cache asynchrone petit mais puissant basé sur Task<TResult>:

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

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

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

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

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

La classe AsyncCache<TKey,TValue> accepte en tant que délégué à son constructeur une fonction qui prend un TKey et retourne un Task<TResult>. Le dictionnaire interne stocke les valeurs précédemment consultées à partir du cache et garantit AsyncCache qu’il génère une seule tâche par clé, même si le cache est accessible simultanément.

Par exemple, vous pouvez créer un cache pour les pages web téléchargées :

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

Vous pouvez ensuite utiliser ce cache dans des méthodes asynchrones chaque fois que vous avez besoin du contenu d’une page web. La AsyncCache classe garantit que vous téléchargez le moins de pages possible et met en cache les résultats.

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

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

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

AsyncProducerConsumerCollection

Vous pouvez également utiliser des tâches pour créer des structures de données pour coordonner les activités asynchrones. Considérez l’un des modèles de conception parallèle classiques : producteur/consommateur. Dans ce modèle, les producteurs génèrent des données que les consommateurs consomment, et les producteurs et les consommateurs peuvent s’exécuter en parallèle. Par exemple, le consommateur traite l’élément 1, qui a été généré précédemment par un producteur qui produit maintenant l’élément 2. Pour le modèle producteur/consommateur, vous avez toujours besoin d’une structure de données pour stocker le travail créé par les producteurs afin que les consommateurs puissent être avertis des nouvelles données et les trouver lorsqu’elles sont disponibles.

Voici une structure de données simple, basée sur des tâches, qui permet aux méthodes asynchrones d’être utilisées en tant que producteurs et consommateurs :

public class AsyncProducerConsumerCollection<T>
{
    private readonly Queue<T> m_collection = new Queue<T>();
    private readonly Queue<TaskCompletionSource<T>> m_waiting =
        new Queue<TaskCompletionSource<T>>();

    public void Add(T item)
    {
        TaskCompletionSource<T>? tcs = null;
        lock (m_collection)
        {
            if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
            else m_collection.Enqueue(item);
        }
        if (tcs != null) tcs.TrySetResult(item);
    }

    public Task<T> Take()
    {
        lock (m_collection)
        {
            if (m_collection.Count > 0)
            {
                return Task.FromResult(m_collection.Dequeue());
            }
            else
            {
                var tcs = new TaskCompletionSource<T>();
                m_waiting.Enqueue(tcs);
                return tcs.Task;
            }
        }
    }
}
Public Class AsyncProducerConsumerCollection(Of T)
    Private ReadOnly m_collection As New Queue(Of T)()
    Private ReadOnly m_waiting As New Queue(Of TaskCompletionSource(Of T))()

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

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

Avec cette structure de données en place, vous pouvez écrire du code comme suit :

static AsyncProducerConsumerCollection<int> m_data = new();

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

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

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

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

L’espace System.Threading.Tasks.Dataflow de noms inclut le BufferBlock<T> type, que vous pouvez utiliser de la même manière, mais sans avoir à créer un type de collection personnalisé :

static BufferBlock<int> m_dataBlock = new();

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

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

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

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

Remarque

L'espace de noms System.Threading.Tasks.Dataflow est disponible en tant que package NuGet. Pour installer l’assembly qui contient l’espace System.Threading.Tasks.Dataflow de noms, ouvrez votre projet dans Visual Studio, choisissez Gérer les packages NuGet dans le menu Projet et recherchez en ligne le System.Threading.Tasks.Dataflow package.

Voir aussi