Programmation asynchrone

Si vous avez des besoins liés aux e/s (par exemple, la demande de données à un réseau, l’accès à une base de données ou la lecture et l’écriture dans un système de fichiers), vous pouvez utiliser la programmation asynchrone. L’écriture de code asynchrone est également indiquée si votre code utilise le processeur de manière intensive, notamment pour effectuer un calcul complexe.

C# possède un modèle de programmation asynchrone au niveau du langage, qui permet d’écrire facilement du code asynchrone sans avoir à jongler avec les rappels ni à se conformer à une bibliothèque qui prend en charge l’asynchronie. Il suit ce que l’on appelle le modèle asynchrone basé sur des tâches (TAP).

Vue d’ensemble du modèle asynchrone

La programmation asynchrone est basée sur les objets Task et Task<T>, qui modélisent les opérations asynchrones. Ces objets sont exposés à l’aide des mots clés async et await. Dans la plupart des cas, le modèle est assez simple :

  • Pour le code lié aux e/s, vous attendez une opération qui retourne un Task ou Task<T> à l’intérieur d’une async méthode.
  • Pour le code lié au processeur, vous attendez une opération démarrée sur un thread d’arrière-plan avec la Task.Run méthode.

Le mot clé await trouve ici toute son utilité. Il cède le contrôle à l’appelant de la méthode qui a effectué l’opération await. Au final, c’est ce qui rend une interface utilisateur réactive ou un service élastique. Bien qu' il existe des façons d’aborder du code asynchrone autre que et await , cet article se concentre sur les constructions de niveau de langage.

Exemple de liaison d’e/s : Télécharger des données à partir d’un service Web

Vous devrez peut-être télécharger des données à partir d’un service Web lorsque vous appuyez sur un bouton mais que vous ne souhaitez pas bloquer le thread d’interface utilisateur. Il peut être accompli de la façon suivante :

private readonly HttpClient _httpClient = new HttpClient();

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 _httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Le code exprime l’intention (en téléchargeant les données de façon asynchrone) sans être coincés dans l’interaction avec les Task objets.

Exemple de processeur dépendant : effectuer un calcul pour un jeu

Supposons que vous développez un jeu pour mobile dans lequel l’appui sur un bouton peut causer des dommages à de nombreux ennemis à l’écran. Le calcul des dommages infligés peut nécessiter beaucoup de ressources. Si ce calcul est effectué sur le thread d’interface utilisateur, le jeu risque d’être considérablement ralenti pendant la durée du calcul.

La meilleure façon de gérer cela est de démarrer un thread d’arrière-plan, qui effectue le travail à l’aide Task.Run de, et d’attendre son résultat à l’aide await de. Cela permet à l’interface utilisateur de paraître lisse au fur et à mesure que le travail est effectué.

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

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

Ce code exprime clairement l’objectif de l’événement Click du bouton, il ne nécessite pas de gérer un thread d’arrière-plan manuellement, et il le fait de façon non bloquante.

Les dessous du code

Du côté du C#, le compilateur transforme votre code en une machine à États qui effectue le suivi d’opérations telles que le traitement de l’exécution quand un await est atteint et la reprise de l’exécution lorsqu’une tâche en arrière-plan est terminée.

Pour théoriquement incliné, il s’agit d’une implémentation du modèle de promesse de l’asynchronie.

Éléments clés à comprendre

  • Le code asynchrone peut être utilisé pour du code utilisant les E/S ou le processeur de manière intensive, mais il est utilisé de manière différente dans chaque scénario.
  • Le code asynchrone utilise les objets Task<T> et Task, qui sont des constructions servant à modéliser le travail effectué en arrière-plan.
  • Le mot clé async définit une méthode comme asynchrone, ce qui vous permet d’utiliser le mot clé await dans le corps de la méthode.
  • Quand le mot clé await est utilisé, il suspend la méthode d’appel et cède le contrôle à son appelant jusqu’à ce que la tâche awaited soit terminée.
  • Le mot clé await peut uniquement être utilisé dans une méthode asynchrone.

Reconnaître le travail lié au processeur et à l’e/s

Les deux premiers exemples de ce guide ont montré comment utiliser async et await pour le travail lié aux e/s et au processeur. C’est la clé que vous pouvez identifier lorsqu’un travail que vous devez effectuer est lié aux e/s ou à l’UC, car cela peut affecter les performances de votre code et peut potentiellement entraîner une utilisation erronée de certaines constructions.

Voici deux questions à vous poser avant d’écrire du code :

  1. Votre code doit-il « attendre » quelque chose, par exemple des données d’une base de données ?

    Si la réponse est « oui », le travail utilise les E/S de manière intensive.

  2. Votre code effectuera-t-il un calcul coûteux ?

    Si la réponse est « oui », le travail utilise le processeur de manière intensive.

Si le travail à faire utilise les E/S de manière intensive, utilisez et awaitawaitTask.Run. Vous ne devez pas utiliser la bibliothèque parallèle de tâches.

Si le travail que vous avez est lié à l' UC et que vous vous souciez de la réactivité, utilisez et await , mais générez le travail sur un autre thread awaitTask.Run . Si le travail est approprié pour la concurrence et le parallélisme, envisagez également d’utiliser la bibliothèque parallèle de tâches.

De plus, vous devez toujours mesurer les performances d’exécution de votre code. Par exemple, vous constaterez peut-être que le coût d’un travail utilisant le processeur de manière intensive n’est pas si élevé que cela par rapport à la surcharge des changements de contexte induits par le multithreading. Chaque solution ayant ses compromis, choisissez le meilleur compromis pour votre scénario.

Autres exemples

Les exemples suivants montrent diverses façons d’écrire du code asynchrone dans C#. Ils correspondent à plusieurs scénarios différents que vous êtes susceptible de rencontrer.

Extraire des données à partir d’un réseau

Cet extrait de code télécharge le code HTML à partir de la page d’accueil https://dotnetfoundation.org de et compte le nombre de fois que la chaîne « .net » se produit dans le code html. il utilise ASP.NET pour définir une méthode de contrôleur d’API Web, qui effectue cette tâche et retourne le nombre.

Notes

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.

private readonly HttpClient _httpClient = new HttpClient();

[HttpGet, Route("DotNetCount")]
public async Task<int> GetDotNetCount()
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org");

    return Regex.Matches(html, @"\.NET").Count;
}

Voici le même scénario écrit pour une application Windows universelle, qui effectue la même tâche quand l’utilisateur appuie 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.
    // This is important to do here, before the "await" call, so that 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 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

Vous pouvez avoir un scénario qui nécessite de récupérer plusieurs éléments de données simultanément. L' Task API contient deux méthodes, Task.WhenAll et Task.WhenAny , qui vous permettent d’écrire du code asynchrone qui exécute une attente non bloquante sur plusieurs travaux en arrière-plan.

Cet exemple vous montre comment récupérer des données User pour plusieurs userId.

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

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

Voici une autre façon de l’écrire de façon plus succincte, à l’aide de LINQ :

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

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

Bien qu’il s’agisse d’un code moins important, soyez prudent lors de la combinaison de LINQ et du code asynchrone. Du fait que LINQ utilise l’exécution différée, les appels asynchrones ne sont pas effectués immédiatement comme c’est le cas dans une boucle foreach, sauf si vous forcez l’itération de la séquence générée avec un appel à .ToList() ou .ToArray().

Informations et conseils importants

Avec la programmation asynchrone, gardez à l’esprit certains détails qui peuvent empêcher un comportement inattendu.

  • asyncles asyncawaitawait

    Il ne faut pas oublier ce point. Si await n’est pas utilisé dans le corps d’une async méthode, le compilateur C# génère un avertissement, mais le code se compile et s’exécute comme s’il s’agissait d’une méthode normale. Cela est incroyablement inefficace, car l’ordinateur d’État généré par le compilateur C# pour la méthode Async n’accomplit rien.

  • Ajoutez « Async » comme suffixe de chaque nom de méthode Async que vous écrivez.

    Il s’agit de la Convention utilisée dans .NET pour 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. Étant donné qu’elles ne sont pas explicitement appelées par votre code, il n’est pas aussi important d’être explicite quant à leur nom.

  • async voidasync void

    L’utilisation de async void est le seul moyen de permettre le fonctionnement des gestionnaires d’événements asynchrones, car les événements n’ont pas de types de retour (et ne peuvent donc pas utiliser les objets Task et Task<T>). Toute autre utilisation de la méthode async void ne suit pas le modèle TAP et peut être difficile à implémenter, comme expliqué ci-après :

    • 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 incorrects si l’appelant ne s’attend pas à ce qu’ils soient asynchrones.
  • Définissez le thread avec précaution si vous utilisez des expressions 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 finir à s’exécuter à un moment où vous ne l’attendiez pas. L’introduction de tâches bloquantes dans ce code peut facilement provoquer un interblocage si le code n’est pas écrit correctement. De plus, l’imbrication de code asynchrone peut rendre la logique d’exécution du code plus compliquée. Async et LINQ sont puissants, mais doivent être utilisés ensemble aussi bien que possible.

  • Écrivez du code qui attend certaines tâches de façon non bloquante.

    Le fait de bloquer le thread actuel comme un moyen d’attendre qu’un se termine peut entraîner des interblocages et bloquer les threads de contexte et peut nécessiter une Task gestion des erreurs plus complexe. Le tableau suivant fournit des conseils sur la façon de traiter l’attente des tâches de façon non bloquante :

    Élément à utiliser... Au lieu de... Lorsque vous souhaitez effectuer cette opération...
    await Task.Wait ou Task.Result Extraire le résultat d’une tâche en arrière-plan
    await Task.WhenAny Task.WaitAny Attendre la fin d’une tâche
    await Task.WhenAll Task.WaitAll Attendre la fin de toutes les tâches
    await Task.Delay Thread.Sleep Attendre pendant une période
  • Envisagez d’utiliserdans la mesure du possible

    Le retour d’un objet Task à partir de méthodes async peut introduire des goulots d’étranglement au niveau des performances dans certains chemins. Task est un type référence. Si vous l’utilisez, vous allouez donc un objet. Dans les cas où 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 devenir un coût de temps significatif dans les sections de code critiques pour les performances. Cela peut devenir coûteux si ces allocations se produisent dans des boucles serrées. Pour plus d’informations, consultez types de retour asynchrones généralisés.

  • Envisagez d’utiliser

    Une question courante est « quand dois-je utiliser la Task.ConfigureAwait(Boolean) méthode ? ». La méthode permet à une Task instance de configurer son await. Il s’agit d’un élément important à prendre en compte et sa définition de manière incorrecte peut avoir une incidence sur les performances et même des blocages. Pour plus d’informations sur ConfigureAwait , consultez le ConfigureAwait.

  • Limitez l’écriture de code avec état.

    Ne dépendez pas 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. Pourquoi ?

    • La logique du code sera plus facile à comprendre.
    • Le code sera plus facile à tester.
    • Combiner du code asynchrone et du code synchrone est beaucoup plus simple.
    • Les concurrences critiques peuvent généralement être évitées.
    • Rendre le code dépendant des valeurs de retour facilite la coordination du code asynchrone.
    • En prime, le code fonctionne parfaitement avec l’injection de dépendances.

L’objectif recommandé est d’atteindre une transparence référentielle complète ou quasi-complète dans votre code. Cela entraînera un code base, pouvant être prévisible, testable et facile à gérer.

Autres ressources