Dot.Blog

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

Task, qui es-tu ? partie 2

[new:30/09/2015]Continuons cette petite série sur Task. Après avoir vu le piège que la nature même de Task tendait, voyons à quoi ressemble cette classe et son instanciation.

Liens rapides vers la série complète

Dis-moi comment tu te construis et je te dirais qui tu es !

Souvent le constructeur, ou les constructeurs, d’une classe en disent beaucoup sur son utilisation à venir. Task n’échappe pas à la règle mais avec une variante intéressante : on n’utilise pourrait ainsi dire jamais son constructeur !

Toutefois l’étude de la construction d’une instance nous apprend des choses intéressantes sur la finalité et les options qui permettent de donner vie à tâche même si ces constructions sont souvent cachées par d’autres mécanismes.

Les constructeurs

Task possède plusieurs constructeurs. Il s’avère que l’équipe en charge de la BCL, la librairie de base de .NET, n’aime pas les paramètres par défaut car ils pensent, avec raison, que ce type d’écriture ne s’accorde pas avec la nature d’une librairie comme .NET et ses évolutions. Dans un logiciel précis il est pratique d’ajouter des valeurs par défaut à certains paramètres, mais lorsqu’on doit gérer les évolutions dans le temps d’une énorme librairies de classes mieux vaut s’en tenir à des constructeurs différenciés clairement. Il est toujours facile d’en ajouter un de plus que d’ajouter un paramètre sans valeur par défaut par exemple (car s’il y a des valeurs par défaut elles sont en fin de liste et on ne peut pas ajouter autre chose d’un paramètre avec valeur par défaut…).

C’est donc pourquoi Task propose 8 constructeurs :

Task(Action);
Task(Action, CancellationToken);
Task(Action, TaskCreationOptions);
Task(Action<Object>, Object);
Task(Action, CancellationToken, TaskCreationOptions);
Task(Action<Object>, Object, CancellationToken);
Task(Action<Object>, Object, TaskCreationOptions);
Task(Action<Object>, Object, CancellationToken, TaskCreationOptions);

 

On pourrait fort bien tous les regrouper en un seul constructeur logique qui possèderait des paramètres par défaut, cela donnerait :

Task(Action action, CancellationToken token = new CancellationToken(), TaskCreationOptions options = TaskCreationOptions.None)
    : this(_ => action(), null, token, options) { }
Task(Action<Object>, Object, CancellationToken = new CancellationToken(), TaskCreationOptions = TaskCreationOptions.None);

 

On notera que ce ne serait pas forcément plus clair d’ailleurs… D’où en parallèle, si je puis dire sur un tel sujet, la réflexion qui s’ouvre sur l’utilisation ou non des paramètres par défaut, les avantages, les inconvénients, etc… je vous laisse y méditer !

Task existe aussi en version générique, Task<T> qui propose la même déclinaison :

Task<TResult>(Func<TResult>);
Task<TResult>(Func<TResult>, CancellationToken);
Task<TResult>(Func<TResult>, TaskCreationOptions);
Task<TResult>(Func<Object, TResult>, Object);
Task<TResult>(Func<TResult>, CancellationToken, TaskCreationOptions);
Task<TResult>(Func<Object, TResult>, Object, CancellationToken);
Task<TResult>(Func<Object, TResult>, Object, TaskCreationOptions);
Task<TResult>(Func<Object, TResult>, Object, CancellationToken, TaskCreationOptions);

 

Liste qu’on pourrait s’amuser à réduire de la même façon à un seul constructeur avec paramètres par défaut :

Task<TResult>(Func<TResult> action, CancellationToken token = new CancellationToken(), TaskCreationOptions options = TaskCreationOptions.None)
    : base(_ => action(), null, token, options) { }
Task<TResult>(Func<Object, TResult>, Object, CancellationToken, TaskCreationOptions);

 

Ce qui n’est franchement pas plus clair, c’est évident !

Task et son double Task<T> arrivent donc avec 16 constructeurs qu’on peut réduire à 2 constructeurs bourrés de paramètres optionnels.

Le choix semble pléthorique … D’autant qu’on n’utilise jamais les constructeurs de Task ou Task<T> !

Pourquoi cette situation à première vue surprenante ?

Tout simplement parce que TPL ou async offrent peu de place à des cas d’utilisation où l’appel direct à un constructeur de Task serait utile…

Comme je l’ai expliqué dans la première partie il y a deux type de Tasks : les Delegate et les Promise Tasks. Les constructeurs de Task ne peuvent pas créer des Promise Tasks, uniquement des Delegate tasks. Ce qui déjà élimine une bonne partie des use cases où le constructeur serait utilisable.

Puisque la logique async fait que le constructeur n’est pas utile (pour le code que vous écrivez en tout cas) il ne peut en toute logique que trouver sa place dans une logique TPL, c’est à dire création de tâches parallèles.

Dans un tel contexte il existe deux grandes familles de tâches, les tâches qui parallélisent des données et celles qui parallélisent du code. Ces dernières peuvent encore se subdiviser en parallélisme statique et parallélisme dynamique. Dans le premier cas le nombre de tâches est fixe et connu à l’avance, dans le second le nombre de tâche peut évoluer en cours d’exécution.

La classe Paralleln PLINQ et les types de la TPL offrent des constructions de haut niveau pour gérer le parallélisme des données et celui du code. La seule véritable occasion de créer un Task se limite donc à lan branche du parallélisme de code dynamique.

Mais même dans ce cas bien particulier il est rare de le faire… le constructeur de Task fabrique une tâche qui n’est pas prête à tourner car elle doit être planifiée avant. Séparer ces deux actions est rarement souhaitable car on désire le plus souvent planifier la tâche immédiatement. La seule raison qui pourrait faire qu’on désire créer une tâche sans la planifiée tout de suite serait de pouvoir laisser le choix d’attribuer ( de planifiée) un thead particulier pour exécuter la tâche. Ce cas est un peu tordu et même là vaudrait-il encore mieux utiliser Func<Task> au lieu de retourner une tâche non planifiée.

Si on résume : si vous voulez faire du parallélisme dynamique et que vous avez besoin de créer des tâches qui peuvent être exécutées sur n’importe quel thread, et que le choix de ce thread est une décision déléguée à une autre partie du code, et que pour une raison à déterminer vous ne pouvez pas utiliser Func<Task>, et bien là, et seulement là vous aurez besoin d’appeler l’un des constructeurs de Task !

Autant dire qu’il s’agit d’une situation tellement spéciale, spécifique, tordue, qu’à l’heure actuelle je n’en ai jamais vu le moindre exemple fonctionnel et n’en ai encore moins jamais écrit.

Vous comprenez pourquoi l’utilisation des constructeurs de Task n’est pas interdite en soi (sinon ils ne seraient pas accessibles, .NET est bien écrit) mais réservée à quelques cas purement hypothétiques que les concepteurs de la BCL n’ont pas voulu … interdire. Ils l’auraient fait que je doute que qui que ce soit aurait été lésé. Les parents finissent en général par le comprendre, parfois mieux vaut cacher que montrer et expliquer pourquoi il ne faut pas toucher ! Sourire

Comment créer des Task alors ?

C’est fort simple ce n’est pas votre problème ! En programmation on dira de façon policée et plus technique “ce n’est pas de la responsabilité de votre code”.

Soit vous écrivez du code async et c’est le mot clé async qui est le moyen le plus simple pour créer du code de type Promise Task. Soit vous englober une autre API asynchrone ou un évènement et vous utilisez Tasl.Factory.FromAsync ou TasCompletionSource<T>. Et si vous avez besoin de faire tourner une tâche en parallèle d’un autre code, vous utilisez Task.Run.

Nous verrons cela dans de prochaines parties. Mais pour du code parallèle commencez en général à regarder du côté  de la classe Parallel ou de PLINQ. Et sauf si vous faites du parallélisme dynamique le mieux est d’utiliser Task.Run ou Task.Factory.StartNew.

Conclusion

Les constructeurs de Task et Task<T> nous apprennent des choses, assurément. On voit de quoi une tâches peut avoir besoin pour tourner et aussi ce qu’on peut attendre. La présence par exemple d’un CancellationToken nous laisse penser qu’il y a des moyens prévus pour annuler une tâches en cours. De même que le paramètre TaskCreationOptions laisse déviner que certains paramètres doivent autoriser un réglage fin de la tâche à créer.

Tout cela est vrai et nous donne une vision sur les entrailles de la bête.

Néanmoins la création directe d’une instance de Task est chose rarissime car il existe des moyens de plus haut niveau pour le faire, et ce sont ces moyens que nous utilisons dans notre code.

Il n’y a pas d’interdiction d’utiliser les constructeurs de Task comme on peut le lire parfois, il y a tout simplement un manque cruel de situations réelles où cela aurait la moindre utilité…

Passons plutôt à la suite, donc…

Stay Tuned !

blog comments powered by Disqus