Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Eviter de marquer async une méthode void

Poster un commentaire

Marquer comme async une méthode void est une pratique courante et presque toujours une mauvaise idée.

Le problème tient en un point:

Si on écrit async void, alors on ne récupère pas de Task

Il devient alors impossible de savoir quand se termine la méthode asynchrone et (peut-être pire) de gérer correctement les éventuels messages d’erreurs. Vous ne pouvez pas non plus gérer l’annulation de la Task, bref vous perdez le contrôle d’une partie de votre code. C’est rarement une bonne nouvelle.

 

Le symptôme typique de cette erreur est que le code fonctionne la plupart du temps. La plupart du temps oui, mais parfois…

 

Je ne pense pas que l’on fasse cette erreur pour le plaisir d’écrire un code qui marche dans 99%, mais pas 100%, mais surtout parce que l’on n’arrive pas à se débrouiller autrement. J’ai moi-même beaucoup bataillé avec ce problème et j’ai donc eu envie de résumer ce que j’ai pu apprendre sur le sujet.

 

Un article très orienté démo dans lequel je vais essayer de présenter quelques situations typiques et une solution possible.

Premier exemple

Le premier exemple est celui ci:

private async void button1_Click(object sender, EventArgs e)
{
   _isSuper = false;
   DoSomething();
 
   Debug.WriteLine(_isSuper);
}
 
private bool _isSuper;
Random _rnd = new Random();
 
private async void DoSomething()
{
   // lance un super truc que nous simulons avec Delay()
   int duree = _rnd.Next(2500);
   await Task.Delay(duree);
 
   // résultat du super truc
   _isSuper = true;
}

Une méthode DoSomething est appelée depuis l’événement Click d’un bouton dans une application Winform. Elle effectue une charge de durée variable dont nous simulons la présence aux lignes 15 et 16.

Le résultat de cette charge est la mise à true de _isSuper.

DoSomething est une méthode void marquée comme async. Il n’est pas difficile de reconstituer ce qui se passe:

  • On lance DoSomething
  • La charge est lancée de façon asynchrone et ligne 16 on rencontre await
  • L’exécution se poursuit ligne 6 en attendant que la charge se termine
    _isSuper est toujours à sa valeur initiale, soit false
  • Le gestionnaire d’événement se termine
  • La charge se termine
  • L’exécution revient à la continuation qui se trouve ligne 19. _isSuper est mis à true
  • DoSomething se termine. Toutefois le code appelant est lui aussi terminé. Le retour ne peut donc être qu’avalé en silence par le Framework Winform.

 

Si DoSomething levait une exception celle-ci serait directement postée dans le thread UI.
Pour une appli Win8 elle serait avalée silencieusement.
WPF affiche un message d’erreur.

 

Quel que soit le contexte, le résultat obtenu n’est pas le résultat attendu.

Puisque le problème vient du fait que le gestionnaire d’événement se termine trop tôt on peut être tenté de le transformer ainsi:

private async void button1_Click(object sender, EventArgs e)
{
   _isSuper = false;
   DoSomething();
   await Task.Delay(2000);
 
   Debug.WriteLine(_isSuper);
}

 

En ajustant un peut la valeur de Delay on peut effectivement arriver à voir ceci:

2015-11-16_21-57-48

En apparence tout va bien, sauf que parfois DoSomething prend un peu plus de temps que prévu et parfois on voit plutôt ceci:

2015-11-16_22-14-33

On pourrait blinder les choses en augmentant la valeur de Delay, mais dans le même temps l’opération globale prend plus de temps. On risque même de passer plus de temps à attendre inutilement le retour de DoSomething qu’à exécuter la méthode.

 

Il est clair que ce code n’est pas le meilleurs possible. En tout cas Delay n’est pas la bonne méthode pour synchroniser le gestionnaire d’événements et DoSomething.

La bonne méthode est de modifier DoSomething pour qu’il retourne une Task et on se sert de Task pour synchroniser ou intercepter correctement les exceptions.

 

Voici comment faire:

private async void button1_Click(object sender, EventArgs e)
{
   _isSuper = false;
   await DoSomethingAsync();
 
   Debug.WriteLine(_isSuper);
}
 
private bool _isSuper;
Random _rnd = new Random();
 
private async Task DoSomethingAsync()
{
   // lance un super truc que nous simulons avec Delay()
   int duree = _rnd.Next(2500);
   await Task.Delay(duree);
 
   // résultat du super truc
   _isSuper = true;
}

 

Par convention, on aime ajouter le suffixe Async à une méthode asynchrone.

Principale différence, DoSomething retourne Task. Du coup on peut placer une déclaration await dans le gestionnaire d’événement. La ligne 6 passe donc dans la continuation de la méthode asynchrone et ne sera exécutée qu’une fois celle-ci terminée.

On constate que cette fois ça fonctionne sans bricolage hasardeux et sans attente inutile.

 

Le problème de cette méthode est que l’on doit pouvoir modifier la signature de DoSomething. Et si ce n’est pas possible?

Deuxième exemple

Je vais reprendre l’exemple du début et prétendre que l’on ne peut pas modifier la signature de DoSomething. Peut être parce que cette méthode est une surcharge (override) d’une méthode de la classe de base du formulaire. Peu importe la raison.

 

On revient donc au problème initial: nous avons besoin d’une Task que l’on peut « awaiter » afin de synchroniser deux actions.

 

L’astuce consiste à créer notre Task avec une méthode ad hoc qui sera invoquée par DoSomething. On aura donc ceci:

private async void button1_Click(object sender, EventArgs e)
{
   _isSuper = false;
   DoSomethingAsync();
   _isSuper = await _isSuperAsync;
 
   Debug.WriteLine(_isSuper);
}
 
Task<bool> _isSuperAsync;
 
private void DoSomethingAsync()
{
   _isSuperAsync = LoadIsSuper();
}

private async Task<bool> LoadIsSuper()
{
   // ici notre charge...
   int duree = _rnd.Next(25000);
   await Task.Delay(duree);
   // résultat du super truc une fois que la charge a fait le boulot
   _isSuper = true;
 
   return _isSuper;
}

Puisque le paramètre que l’on attend est un bool (_isSuper), nous allons créer une Task<bool>. La présence de Task nous assure de pouvoir faire un await comme on le voit dans le gestionnaire d’événement.

On ne peut pas retourner une Task, mais on peut créer une propriété de type Task. C’est ce que fait LoadIsSuper.

 

Le reste n’est pas compliqué à reconstituer.

Conclusions

La règle générale est d’éviter autant que possible de marquer async une méthode void car on ne peut plus contrôler sa continuation et on ne reçoit plus correctement les exceptions qu’elle lève.

C’est le fait qu’une méthode asynchrone retourne une Task qui nous permet ce contrôle.

 

Il y a toutefois des exceptions, la principale sont les gestionnaires d’événements. .Net met en place une plomberie interne pour gérer correctement cette situation.

 

Examinons une situation plus subtile qui concerne les méthodes anonymes qui peuvent elles aussi être async:


Action action1 = async () => { await LoadIsSuper(); };
Func<Task> action2 = async () => { await LoadIsSuper(); };

A votre avis, le compilateur va choisir void (action1) ou bien Task (action2)?

On pourrait se dire qu’en l’absence de return dans la méthode anonyme, le plus logique est de renvoyer void. Seulement on tombe dans les affres d’une async void et on a vu que l’on doit l’éviter.

C’est d’ailleurs précisément ce que va faire le compilateur. La signature choisir sera celle de action2.

On a par contre une réelle différence avec VB qui distingue au niveau de la syntaxe entre une méthode et une fonction. Dans ce cas on peut choisir l’une ou l’autre signature. Mais dit t’on le faire…

 

Bibliographie

http://blogs.msdn.com/b/lucian/archive/2013/11/23/talk-mvp-summit-async-best-practices.aspx

 

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s