Partager via


Scénarios de la programmation asynchrone

Si votre code implémente des scénarios liés aux E/S pour prendre en charge les demandes de données réseau, l’accès à la base de données ou les écritures de système de fichiers, la programmation asynchrone est la meilleure approche. Vous pouvez également écrire du code asynchrone pour des scénarios liés au processeur, comme des calculs coûteux.

C# a un modèle de programmation asynchrone au niveau du langage qui vous permet d’écrire facilement du code asynchrone sans avoir à jugler les rappels ou à se conformer à une bibliothèque qui prend en charge l’asynchronie. Le modèle suit ce qu’on appelle le modèle asynchrone basé sur les tâches (TAP).

Explorer le modèle de programmation asynchrone

Les Task objets et Task<T> les objets représentent le cœur de la programmation asynchrone. Ces objets sont utilisés pour modéliser des opérations asynchrones en prenant en charge les mots clés et await les async mots clés. Dans la plupart des cas, le modèle est assez simple pour les scénarios liés aux E/S et au processeur. À l’intérieur d’une async méthode :

  • Le code lié aux E/S démarre une opération représentée par un ou Task<T> un Task objet dans la async méthode.
  • Le code lié au processeur démarre une opération sur un thread d’arrière-plan avec la Task.Run méthode.

Dans les deux cas, un actif Task représente une opération asynchrone qui peut ne pas être terminée.

Le mot clé await trouve ici toute son utilité. Il génère un contrôle à l’appelant de la méthode qui contient l’expression await , et permet finalement à l’interface utilisateur d’être réactive ou d’un service d’être élastique. Bien qu’il existe des façons d’aborder le code asynchrone autre que l’utilisation des expressions et await des async expressions, cet article se concentre sur les constructions au niveau du langage.

Remarque

Certains exemples présentés dans cet article utilisent la System.Net.Http.HttpClient classe pour télécharger des données à partir d’un service web. Dans l’exemple de code, l’objet s_httpClient est un champ statique de classe de type Program :

private static readonly HttpClient s_httpClient = new();

Pour plus d’informations, consultez l’exemple de code complet à la fin de cet article.

Passer en revue les concepts sous-jacents

Lorsque vous implémentez la programmation asynchrone dans votre code C#, le compilateur transforme votre programme en ordinateur d’état. Cette construction effectue le suivi de différentes opérations et états dans votre code, telles que la sortie de l’exécution lorsque le code atteint une await expression et la reprise de l’exécution lorsqu’un travail en arrière-plan se termine.

En termes de théorie de la science informatique, la programmation asynchrone est une implémentation du modèle Promise d’asynchronie.

Dans le modèle de programmation asynchrone, il existe plusieurs concepts clés à comprendre :

  • Vous pouvez utiliser du code asynchrone pour le code lié aux E/S et le code lié au processeur, mais l’implémentation est différente.
  • Le code asynchrone utilise et Task les Task<T> objets comme constructions pour modéliser le travail en cours d’exécution en arrière-plan.
  • Le async mot clé déclare une méthode en tant que méthode asynchrone, ce qui vous permet d’utiliser le await mot clé dans le corps de la méthode.
  • Lorsque vous appliquez le await mot clé, le code suspend la méthode appelante et retourne le contrôle à son appelant jusqu’à ce que la tâche se termine.
  • Vous ne pouvez utiliser l’expression await que dans une méthode asynchrone.

Exemple lié aux E/S : Télécharger des données à partir du service web

Dans cet exemple, lorsque l’utilisateur sélectionne un bouton, l’application télécharge les données à partir d’un service web. Vous ne souhaitez pas bloquer le thread d’interface utilisateur de l’application pendant le processus de téléchargement. Le code suivant effectue cette tâche :

s_downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await s_httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Le code exprime l’intention (télécharger des données de façon asynchrone) sans que cela nécessite des interactions compliquées avec les objets Task.

Exemple lié au processeur : Exécuter le calcul du jeu

Dans l’exemple suivant, un jeu mobile inflige des dommages à plusieurs agents sur l’écran en réponse à un événement de bouton. L’exécution du calcul des dommages peut être coûteuse. L’exécution du calcul sur le thread d’interface utilisateur peut entraîner des problèmes d’affichage et d’interaction de l’interface utilisateur pendant le calcul.

La meilleure façon de gérer la tâche consiste à démarrer un thread d’arrière-plan pour terminer le travail avec la Task.Run méthode. L’opération génère à l’aide d’une await expression. L’opération reprend une fois la tâche terminée. Cette approche permet à l’interface utilisateur de s’exécuter correctement pendant que le travail se termine en arrière-plan.

static DamageResult CalculateDamageDone()
{
    return new DamageResult()
    {
        // Code omitted:
        //
        // Does an expensive calculation and returns
        // the result of that calculation.
    };
}

s_calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work. The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

Le code exprime clairement l’intention de l’événement de bouton Clicked . Il ne nécessite pas de gestion manuelle d’un thread d’arrière-plan et termine la tâche de manière non bloquante.

Reconnaître les scénarios liés au processeur et aux E/S

Les exemples précédents montrent comment utiliser le modificateur et await l’expression pour le async travail lié aux E/S et au processeur. Un exemple pour chaque scénario montre comment le code est différent en fonction de l’emplacement où l’opération est liée. Pour préparer votre implémentation, vous devez comprendre comment identifier quand une opération est liée à des E/S ou liées au processeur. Votre choix d’implémentation peut considérablement affecter les performances de votre code et entraîner une mauvaise utilisation des constructions.

Il existe deux questions principales à résoudre avant d’écrire du code :

Question Scénario Implémentation
Le code doit-il attendre un résultat ou une action, comme les données d’une base de données ? Lié aux E/S Utilisez le modificateur et await l’expression asyncsans la Task.Run méthode.

Évitez d’utiliser la bibliothèque parallèle de tâches.
Le code doit-il exécuter un calcul coûteux ? Lié à l’UC Utilisez le modificateur et await l’expressionasync, mais générez le travail sur un autre thread avec la Task.Run méthode. Cette approche traite des problèmes liés à la réactivité du processeur.

Si le travail est approprié pour la concurrence et le parallélisme, envisagez également d’utiliser la bibliothèque parallèle de tâches.

Mesurez toujours l’exécution de votre code. Vous pouvez découvrir que votre travail lié au processeur n’est pas suffisamment coûteux par rapport à la surcharge des commutateurs de contexte lors de la multithreading. Chaque choix a des compromis. Choisissez le compromis approprié pour votre situation.

Explorer d’autres exemples

Les exemples de cette section illustrent plusieurs façons d’écrire du code asynchrone en C#. Ils couvrent quelques scénarios que vous pouvez rencontrer.

Extraire des données d’un réseau

Le code suivant télécharge le code HTML à partir d’une URL donnée et compte le nombre de fois où la chaîne .NET se produit dans le code HTML. Le code utilise ASP.NET pour définir une méthode de contrôleur d’API web, qui effectue la tâche et retourne le nombre.

Remarque

Si vous prévoyez d’effectuer une analyse HTML dans le code de production, n’utilisez pas d’expressions régulières. Utilisez plutôt une bibliothèque d’analyse.

[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await s_httpClient.GetStringAsync(URL);
    return Regex.Matches(html, @"\.NET").Count;
}

Vous pouvez écrire du code similaire pour une application Windows universelle et effectuer la tâche de comptage après une pression sur un bouton :

private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
    // Capture the task handle here so we can await the background task later.
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");

    // Any other work on the UI thread can be done here, such as enabling a Progress Bar.
    // It's important to do the extra work here before the "await" call,
    // so the user sees the progress bar before execution of this method is yielded.
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
    // This action is what allows the app to be responsive and not block the UI thread.
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, @"\.NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visibility = Visibility.Collapsed;
}

Attendre la fin de plusieurs tâches

Dans certains scénarios, le code doit récupérer plusieurs éléments de données simultanément. Les Task API fournissent des méthodes qui vous permettent d’écrire du code asynchrone qui effectue une attente non bloquante sur plusieurs travaux en arrière-plan :

L’exemple suivant montre comment récupérer User des données d’objet pour un ensemble d’objets userId .

private static async Task<User> GetUserAsync(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.

    return await Task.FromResult(new User() { id = userId });
}

private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

Vous pouvez écrire ce code plus succinctement à l’aide de LINQ :

private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
    return await Task.WhenAll(getUserTasks);
}

Bien que vous écriviez moins de code à l’aide de LINQ, soyez prudent lors du mélange de LINQ avec du code asynchrone. LINQ utilise l’exécution différée (ou différée). Les appels asynchrones ne se produisent pas immédiatement comme ils le font dans une foreach boucle, sauf si vous forcez la séquence générée à itérer avec un appel à la méthode ou .ToArray() à l’appel.ToList(). Cet exemple utilise la Enumerable.ToArray méthode pour effectuer la requête avec impatience et stocker les résultats dans un tableau. Cette approche force l’instruction id => GetUserAsync(id) à s’exécuter et à lancer la tâche.

Examiner les considérations relatives à la programmation asynchrone

Avec la programmation asynchrone, il existe plusieurs détails à garder à l’esprit qui peuvent empêcher un comportement inattendu.

Utiliser await dans le corps de la méthode async()

Lorsque vous utilisez le async modificateur, vous devez inclure une ou plusieurs await expressions dans le corps de la méthode. Si le compilateur ne rencontre pas d’expression await , la méthode ne parvient pas à générer. Bien que le compilateur génère un avertissement, le code se compile toujours et le compilateur exécute la méthode. L’ordinateur d’état généré par le compilateur C# pour la méthode asynchrone n’accomplit rien, de sorte que l’ensemble du processus est très inefficace.

Ajouter le suffixe « Async » aux noms de méthodes asynchrones

La convention de style .NET consiste à ajouter le suffixe « Async » à tous les noms de méthode asynchrones. Cette approche permet de différencier plus facilement les méthodes synchrones et asynchrones. Certaines méthodes qui ne sont pas explicitement appelées par votre code (telles que les gestionnaires d’événements ou les méthodes de contrôleur web) ne s’appliquent pas nécessairement dans ce scénario. Étant donné que ces éléments ne sont pas explicitement appelés par votre code, l’utilisation d’un nommage explicite n’est pas aussi importante.

Renvoyer 'async void' uniquement à partir de gestionnaires d’événements

Les gestionnaires d’événements doivent déclarer void des types de retour et ne peuvent pas utiliser ou retourner Task des Task<T> objets comme d’autres méthodes. Lorsque vous écrivez des gestionnaires d’événements asynchrones, vous devez utiliser le async modificateur sur une void méthode de retour pour les gestionnaires. D’autres implémentations de retour de async void méthodes ne suivent pas le modèle TAP et peuvent présenter des défis :

  • Les exceptions levées dans une async void méthode ne peuvent pas être interceptées en dehors de cette méthode
  • async void les méthodes sont difficiles à tester
  • async void les méthodes peuvent provoquer des effets secondaires négatifs si l’appelant ne s’attend pas à ce qu’ils soient asynchrones

Utilisez une prudence avec des lambda asynchrones dans LINQ

Il est important d’utiliser la prudence lorsque vous implémentez des lambda asynchrones dans des expressions LINQ. Les expressions lambda dans LINQ utilisent l’exécution différée, ce qui signifie que le code peut s’exécuter à un moment inattendu. L’introduction de tâches bloquantes dans ce scénario peut facilement entraîner un blocage, si le code n’est pas écrit correctement. En outre, l’imbrication du code asynchrone peut également compliquer l’exécution du code. Async et LINQ sont puissants, mais ces techniques doivent être utilisées ensemble aussi soigneusement et clairement que possible.

Rendement des tâches de manière non bloquante

Si votre programme a besoin du résultat d’une tâche, écrivez du code qui implémente l’expression await de manière non bloquante. Le blocage du thread actuel comme moyen d’attendre de façon synchrone pour qu’un Task élément se termine peut entraîner des blocages et des threads de contexte bloqués. Cette approche de programmation peut nécessiter une gestion des erreurs plus complexe. Le tableau suivant fournit des conseils sur la façon dont les résultats d’accès à partir de tâches de manière non bloquante :

Scénario de tâche Code actuel Remplacer par « await »
Récupérer le résultat d’une tâche en arrière-plan Task.Wait ou Task.Result await
Continuer à l’exécution d’une tâche Task.WaitAny await Task.WhenAny
Continuer lorsque toutes les tâches se terminent Task.WaitAll await Task.WhenAll
Continuer après un certain temps Thread.Sleep await Task.Delay

Envisagez d’utiliser le type ValueTask

Lorsqu’une méthode asynchrone retourne un Task objet, des goulots d’étranglement des performances peuvent être introduits dans certains chemins d’accès. Étant donné qu’il Task s’agit d’un type référence, un Task objet est alloué à partir du tas. Si une méthode déclarée avec le async modificateur retourne un résultat mis en cache ou se termine de façon synchrone, les allocations supplémentaires peuvent accumuler des coûts de temps significatifs dans les sections critiques de performances du code. Ce scénario peut devenir coûteux lorsque les allocations se produisent dans des boucles serrées. Pour plus d’informations, consultez Types de retour asynchrones généralisés.

Comprendre quand définir ConfigureAwait(false)

Les développeurs se demandent souvent quand utiliser la Task.ConfigureAwait(Boolean) valeur booléenne. Cette API permet à une Task instance de configurer le contexte de l’ordinateur d’état qui implémente n’importe quelle await expression. Lorsque la valeur booléenne n’est pas définie correctement, les performances peuvent dégrader ou bloquer des blocages. Pour plus d’informations, consultez la FAQ ConfigureAwait.

Écrire du code moins avec état

Évitez d’écrire du code qui dépend de l’état des objets globaux ou de l’exécution de certaines méthodes. Le code doit uniquement dépendre des valeurs de retour des méthodes. Il existe de nombreux avantages pour écrire du code qui est moins avec état :

  • Plus facile à raisonner sur le code
  • Plus facile à tester le code
  • Plus simple pour combiner du code asynchrone et synchrone
  • Capable d’éviter les conditions de concurrence dans le code
  • Code asynchrone simple à coordonner qui dépend des valeurs de retour
  • (Bonus) Fonctionne bien avec l’injection de dépendances dans le code

L’objectif recommandé est d’atteindre une transparence référentielle complète ou quasi-complète dans votre code. Cette approche entraîne une base de code prévisible, testable et maintenable.

Passez en revue l’exemple complet

Le code suivant représente l’exemple complet, qui est disponible dans l’exemple de fichier Program.cs .

using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;

class Button
{
    public Func<object, object, Task>? Clicked
    {
        get;
        internal set;
    }
}

class DamageResult
{
    public int Damage
    {
        get { return 0; }
    }
}

class User
{
    public bool isEnabled
    {
        get;
        set;
    }

    public int id
    {
        get;
        set;
    }
}

public class Program
{
    private static readonly Button s_downloadButton = new();
    private static readonly Button s_calculateButton = new();

    private static readonly HttpClient s_httpClient = new();

    private static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/shows/net-core-101/what-is-net",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://dotnetfoundation.org",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            "https://learn.microsoft.com/maui"
    };

    private static void Calculate()
    {
        // <PerformGameCalculation>
        static DamageResult CalculateDamageDone()
        {
            return new DamageResult()
            {
                // Code omitted:
                //
                // Does an expensive calculation and returns
                // the result of that calculation.
            };
        }

        s_calculateButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI while CalculateDamageDone()
            // performs its work. The UI thread is free to perform other work.
            var damageResult = await Task.Run(() => CalculateDamageDone());
            DisplayDamage(damageResult);
        };
        // </PerformGameCalculation>
    }

    private static void DisplayDamage(DamageResult damage)
    {
        Console.WriteLine(damage.Damage);
    }

    private static void Download(string URL)
    {
        // <UnblockingDownload>
        s_downloadButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI as the request
            // from the web service is happening.
            //
            // The UI thread is now free to perform other work.
            var stringData = await s_httpClient.GetStringAsync(URL);
            DoSomethingWithData(stringData);
        };
        // </UnblockingDownload>
    }

    private static void DoSomethingWithData(object stringData)
    {
        Console.WriteLine($"Displaying data: {stringData}");
    }

    // <GetUsersForDataset>
    private static async Task<User> GetUserAsync(int userId)
    {
        // Code omitted:
        //
        // Given a user Id {userId}, retrieves a User object corresponding
        // to the entry in the database with {userId} as its Id.

        return await Task.FromResult(new User() { id = userId });
    }

    private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
    {
        var getUserTasks = new List<Task<User>>();
        foreach (int userId in userIds)
        {
            getUserTasks.Add(GetUserAsync(userId));
        }

        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDataset>

    // <GetUsersForDatasetByLINQ>
    private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
    {
        var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDatasetByLINQ>

    // <ExtractDataFromNetwork>
    [HttpGet, Route("DotNetCount")]
    static public async Task<int> GetDotNetCount(string URL)
    {
        // Suspends GetDotNetCount() to allow the caller (the web server)
        // to accept another request, rather than blocking on this one.
        var html = await s_httpClient.GetStringAsync(URL);
        return Regex.Matches(html, @"\.NET").Count;
    }
    // </ExtractDataFromNetwork>

    static async Task Main()
    {
        Console.WriteLine("Application started.");

        Console.WriteLine("Counting '.NET' phrase in websites...");
        int total = 0;
        foreach (string url in s_urlList)
        {
            var result = await GetDotNetCount(url);
            Console.WriteLine($"{url}: {result}");
            total += result;
        }
        Console.WriteLine("Total: " + total);

        Console.WriteLine("Retrieving User objects with list of IDs...");
        IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
        var users = await GetUsersAsync(ids);
        foreach (User? user in users)
        {
            Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
        }

        Console.WriteLine("Application ending.");
    }
}

// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.