Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Task, qui es-tu ? partie 11

La partie 10 a permis de visiter les moyens de créer des tâches de type Delegate, il reste à voir comment faire de même avec des tâches de type Promise qui sont en réalité des sortes d’évènements sans code à exécuter.

Liens rapides vers la série complète

Delegate vs Promise

La nuance entre ces deux types de tâche a été traitée dans cette série. Le lecteur qui prendrait cette dernière en plein milieu aura donc tout intérêt à partir depuis le premier article pour être sûr de profiter pleinement de cette édifiante lecture !

Task.Delay

Task.Delay est en quelque sorte la contrepartie asynchrone de Thread.Sleep.

Les variantes sont peu nombreuses et explicites :

Task Delay(int);
Task Delay(TimeSpan);

Task Delay(int, CancellationToken);
Task Delay(TimeSpan, CancellationToken);

 

L’argument entier est exprimé en millisecondes. Dans un code bien écrit on préfèrera la version avec TimeSpan qui clarifie les intentions.

Dans la pratique Task.Delay créée un timer et exécute la tâche une fois celui-ci arrivé en fin de décompte. Sauf si entre temps le jeton d’annulation a été utilisé pour annuler la tâche. Ici encore le jeton ne concerne que le démarrage de la tâche. Une fois celle-ci lancée le jeton n’a aucun effet…

L’utilité de Task.Delay est très limité. Cela peut être intéressant pour gérer le classique problème des timeouts mais dans un contexte asynchrone. Si une opération possédant un timeout échoue on tenter de la reprogrammer pour plus tard en utilisant un Task.Delay ce qui est conforme au modèle de programmation asynchrone (au lieu d’un affreux Thread.Sleep).

Toutefois les logiques de type retry sont plutôt le rôle de librairie comme Transient Fault Handling ou Polly et ce sont ces librairies qui en interne utilisent Task.Delay, rarement le développeur directement dans son code.

Les librairies que je viens d’évoquer sont d’une extrême importance bien qu’elles soient peu connues. Elles offrent des moyens puissants et élégants pour gérer les erreurs transitoires de type timeout par exemple avec des retry, retry forever, wait etc… Pour avoir souvent écrit du code de ce type je ne peux que vous conseiller d’utiliser ces librairies car ce qui semble être un problème simple est en réalité un véritable casse-tête dont la gestion alourdit inutilement le code applicatif. Polly est un projet Github alors que TFH est un projet de l’excellente équipe de Patterns & Practices de Microsoft bien documenté comme d’habitude. Je vous conseille d’ailleurs la lecture de leur introduction à la librairie qui pose de façon claire tout le contexte des erreurs dites transitoires. Un must à connaitre (et à utiliser !).

Task.Yield

Le but de cette série n’est pas d’être exhaustive mais pratique au sens qu’elle donne des informations pour utiliser au mieux Task.

Et nous l’avons vu finalement avec Task.Run on a à peu près tout ce qu’il faut pour lancer des tâches … Tout le reste est d’utilisation occasionnelle dans des contextes hyper pointus ou bien de fausses bonnes idées dans un code moderne et réactif.

Dépoussiérer Task de toutes ces méthodes et “on dit” qui ne servent à rien est néanmoins très utile.

Dans ce cadre il faut parler de Task.Yield.

Cette méthode ne retourne pas une Task mais un YieldAwaitable, une sorte de Promise Task qui agit avec le compilateur pour forcer un point asynchrone à l’intérieur d’une méthode.

En dehors de code exemple pour tester la feature je n’ai jamais vu aucune utilisation de Task.Yield en production. C’est un truc exotique…

Certains pensent en avoir compris l’effet et se disent que c’est un moyen intéressant pour placer des points “d’air’ dans une boucle par exemple afin que l’UI puisse se rafraichir (ou d’autres cas du même genre). Dans la boucle on voit ainsi apparaitre un await Task.Yield() qui fait très savant… Mais qui ne sert à rien du tout. Dans le cas de l’UI elle s’exécute sur un thread prioritaire, bien plus que les messages WM_PAINT de Windows… Donc oui il y a bien une sorte de “prise d’air” créée par le Yield mais elle ne sert à rien.

La bonne méthode pour ce genre de situation mérite d’être abordée puisque cela nous permet de rester dans le sujet des Task :

Au lieu d’un code de ce type

// MAUVAIS CODE !
async Task LongRunningCpuBoundWorkAsync()
{
  // Méthode appelée sur le thread de l'UI
  //  effectuant un travail CPU intensif.
  for (int i = 0; i != 1000000; ++i)
  {
    ... // CPU-bound work.
    await Task.Yield();
  }
}

 

Il faut bien entendu séparer le code long ou CPU intensif du code traité par le thread de l’UI. Ce qui devient dans une version propre qui elle fonctionne en plus, et en asynchrone :

void LongRunningCpuBoundWork()
{
  for (int i = 0; i != 1000000; ++i)
  {
    ... // CPU-bound work.
  }
}

// Appelé comme suit
await Task.Run(() => LongRunningCpuBoundWork());

 

En bref : n’utilisez pas Task.Yield.

Task.FromResult

Là encore on touche l’un des nombreux exotismes de Taks qui en rend l’approche douloureuse alors que tout cela ne sert pas à grand chose…

En gros FromResult permet de retourner une Task qui serait déjà terminée avant même d’avoir commencé. Cela n’existe pas. Donc à quoi cela peut-il servir ? En réalité il y un petit créneau d’utilisation : l’écriture de stubs pour le Unit Testing par exemple. Mais ce n’est pas du code de production et on peut parfaitement utiliser Task sans rien comprendre ni jamais utiliser FromResult.

Le cas d’utilisation typique et quasi unique de FromResult est celui dans lequel on a une interface qui prévoit une méthode de type async retournant une tâche mais qu’on dispose d’une implémentation synchrone qui ne fait que retourner la valeur. Pour “adapter” le code synchrone à la signature de l’interface, on utilise FromResult sur le résultat déjà calculé ce qui va créer une Task<TResult> qui conviendra parfaitement à l’interface, sa signature et les méthodes qui font un await dessus…

Même dans ce cas bien particulier il faut faire attention à des petits détails comme le fait que le code synchrone ainsi enrubanné par FromResult ne soit pas bloquant… Utilisé un code bloquant dans une méthode asynchrone réserve plein de mauvaises surprises. On peut donc utiliser FromResult pour retourner un résultat déjà existant par exemple, mais s’il doit prendre un peu de temps à obtenir alors autant respecter la signature de l’interface et implémenter un véritable code asynchrone…

Je parlais d’un cas d’utilisation “quasi unique” cela laisse entendre qu’il y aurait peut être d’autres utilisations de FromResult. Mais lesquelles ?

On en trouve une dans le cadre d’un système de cache de valeurs qui fonctionne en mode asynchrone. Dans un tel contexte on retourne des Task<T>, donc si la valeur est déjà dans le cache il faut la transformer en Task<T> bien qu’il n’y ait aucun code à exécuter, c’est une tâche terminée avant d’avoir commencée. Et si la valeur n’est pas dans le cache on appelle la méthode qui la calcule et qui elle est une vraie tâche retournant aussi un Task<T> avec du vrai code à exécuter. Ce qui pourrait donner quelque chose comme :

public Task<string> GetValueAsync(int key)
{
  string result;
  if (cache.TryGetValue(key, out result))
    return Task.FromResult(result);
  return DoGetValueAsync(key);
}

private async Task<string> DoGetValueAsync(int key)
{
  string result = await ...;
  cache.TrySetValue(key, result);
  return result;
}

 

On notera la présence dans .NET 4.6 d’une propriété Task.CompletedTask dont le rôle est justement de permettre le retour d’une valeur “immédiate” avec une tâche dont le statut est bien RanToCompletion, simulant parfaitement une Task<T> mais sans code à exécuter. Dans le code ci-dessus on utilisa donc plutôt un Task.CompletedTask au lieu d’un FromResult. La documentation de .NET 4.6 ne montre aucun exemple de la syntaxe et Google a beau être mon ami, il ne m’en dit pas plus, tout comme Bing. Il faudra attendre un peu pour en savoir plus…

Dans le même esprit .NET 4.6 offre des moyens de retourner des tâches en mode Faulted ou Canceled sans aucune exécution de code (FromCanceled et FromException). Cela peut être intéressant dans certaines situations où il est important de conserver un écriture asynchrone.

Conclusion

Comprendre Task est essentiel. Cette série, du moins je l’espère, vous aura montré que sont utilisation n’est pas aussi complexe que le laisse supposer les nombreuses méthodes de cette classe. S’il faut savoir à quoi sert Task.Yield, FromResult, StartNew etc, on s’aperçoit bien vite qu’au final seul Task.Run (et Run<T>) est véritablement utile au quotidien dans du code de production.

Laissez tomber les complexes face aux kékés qui à la machine à café viendront jouer les savants, maintenant vous savez ce qu’est Task et comment s’en servir… Tout est intéressant à savoir, mais ici peu est à savoir pour bien s’en servir…

J’ai bien conscience de n’avoir pas épuisé le sujet, mais ma prétention n’était pas de faire un cours sur l’asynchonisme, juste de vous parler de la classe Task. Mais j’y reviendrais forcément…

var whatForNow = await Task.FromResult(“Stay Tuned !”);

blog comments powered by Disqus