Ceci est un exemple montrant comment il est possible de réaliser des calculs en parallèle avec la mécanique reposant sur async et await en C#.
L'idée de base de cette mécanique est somme toute simple.
Le programme de test est identique dans chaque cas. | Synchrone (sens faible) | Asynchrone |
---|---|---|
|
|
|
Si une méthode retourne normalement un T lorsqu'elle s'exécute de manière synchrone, elle peut s'exécuter de manière asynchrone si on la qualifie par le mot clé async et si elle retourne plutôt une Task<T>. Il faut par contre que la version asynchrone contienne une mise en attente (avec await); une fonction qui ne rencontre pas ces deux critères aura un comportement synchrone au sens faible du terme : l.'appelant sera suspendu pendant l'exécution de l'appelé. |
|
|
Il est possible d'appeler les versions asynchrones des services de manière semblable à un appel synchrone, à ceci près que l'appel initial provoque le démarrage du traitement, pas nécessairement sa complétion. Pour cette raison, dans la déclinaison asynchrone, un Task<T> est retourné, pas un T. Le Task<T> est en fait une sorte de future, qui connaît le contexte de l'appel en cours et est conscient de la complétion (ou non) du traitement qui lui est associé. Ici, pour réduire au minimum le temps de traitement, la version asynchrone attend (Task.WaitAll) la complétion de l'ensemble des tâches lancées dans Calcul, et retourne la somme des résultats obtenus. Ce faisant, les traitements se font en parallèle. |
|
|
Ce qui est amusant dans cet exemple est que la fonction Calcul encapsule le caractère synchrone ou non du calcul, ce qui explique (comme mentionné plus haut) que le programme de test soit identique dans chaque cas... |
|
|
... mais la vitesse d'exécution n'est pas la même! On remarquera toutefois dans la version asynchrone le coût du lancement des threads associés aux tâches, qui n'est pas négligeable. |
|
|
Notez qu'une fonction qui fait un await doit être marquée async. Ici, dans la version asynchrone, l'appel à Task<int>.WaitAll est le lieu où la dernière mise en attente s'effectue; d'autres options existent, mais un appel « bête » à f sans await dans Calcul aurait provoqué un avertissement du compilateur, indiquant que ceci transforme le calcul réalisé en calcul synchrone...
Il est facile de se mettre dans le pétrin par une mauvaise compréhension des mécanismes tels que ceux induits par async et await. Voici un bref exemple d'une erreur trop facile à faire. Reprenons les versions asynchrones des fonctions F et G vues précédemment.
|
|
On pourrait être tenté d'appeler F(1) et G(1) avec await dans chaque cas, puis de retourner la somme de f_ et g_ (deux Task<int>). Ceci est logique et fonctionne : le résultat calculé sera le bon. Toutefois, retournant désormais une Task<int> dont le résultat n'est peut-être pas encore calculé, il nous faut alors qualifier Calcul() du mot clé async. |
|
En conservant pratiquement le même programme de test, à ceci près que Calcul() retourne ici une Task<int> nommée localement tâche, et que le résultat effectif est tiré de tâche.Result. Un programme de test possible serait :
Cependant, la vitesse d'exécution est alors... décevante. Pourquoi donc? |
|
Le problème se situe dans Calcul, où await F(1) complète temporairement Calcul et retourne un Task<int> « conscient » du contexte d'exécution asynchrone courant. L'exécution de Calcul reprend suite à la complétion de F(1) pour se remettre en attente de G(1). Conséquemment, l'évaluation de tâche.Result dans le programme principal bloquera pour un temps proportionnel à la somme des temps de calcul de F(1) et de G(1), une vitesse d'exécution équivalente à celle d'une séquence d'appels synchrones avec un peu d'effort supplémentaire pour gérer les calculs en arrière-plan. Cette approche n'est donc pas à retenir dans un cas comme celui-ci. |
|
Pour en savoir plus sur le sujet, quelques suggestions de lecture complémentaire sur async et await :