Dot.Blog

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

Rx Extensions, TPL et Async CTP : L’asynchronisme arrive en parallèle !

[new: 15/12/2011]Les RX Extensions, TPL et Async CTP sont trois technologies releasées ou en cours de l’être, toutes les trois traitent d’asynchronisme et de parallélisme. Toutes les trois déboulent presque en même temps, ce qui est une belle illustration d’auto-référence ! Mais en dehors de ça, comment comprendre cette avalanche et que choisir ?

Ils sont fous ces Microsoftiens !

Un peu comme les Romains d’Astérix et Obélix se tapent la tête contre les murs essayant vainement de comprendre quelle logique anime ces satanés Gaulois, le développeur s’arrache un peu les cheveux devant un tel tir groupé de trois technologies concurrentes chez le même éditeur...

Pourquoi trois procédés proches, quelles sont les différences entre eux, lequel choisir ?

Pour les lecteurs pressés

Pour ceux qui veulent tout savoir sans rien lire, on pourrait faire une version courte qui serait :

  • Les Reactive Extensions (Rx) sont des opérateurs pour travailler sur des flux de données
  • TPL (Task Parallel Library) est une sorte de ThreadPool sous stéroïdes
  • L’Async CTP c’est TPL encore plus dopé.

Ce n’est pas forcément avec ça que vous en saurez beaucoup plus, mais quand on est très pressé, forcément, on se contente de bribes d’informations...

Pour ceux qui veulent mieux comprendre, heureusement je vais écrire une suite Sourire

La concurrence sous .NET

La concurrence désigne un état dans lequel plusieurs code différents tournent en même temps, généralement au sein d’un même processus (au sens large) et qui accèdent à des ressources communes (d’où la concurrence, sinon il s’agit de simple multitâche). Par exemple, les applications qui tournent en même en temps sous Windows sont du code concurrent vis à vis de l’OS ou du processeur, même si chacune ne gère pas du tout de multitâche. L’OS doit gérer le fait que les applications vont accéder aux mêmes ressources comme l’écran, les imprimantes...

Sous .NET, et au sein d’une même application, on parlera de concurrence dans plusieurs cas précis :

  • Une tâche est créée et démarrée en utilisant la classe Thread
  • Une ou plusieurs tâches sont crées et démarrées en utilisant la classe ThreadPool
  • Des tâches asynchrones sont exécutées comme une invocation de délégué par BeginInvoke, des opérations d’E/S au travers du ThreadPool, etc.
  • Des tâches sont asynchrones par nature dans un environnement donné comme par exemple les Ria Services ou toute communication Wcf avec Silverlight, ou bien le choix est fait d’un tel asynchronisme dans des environnements comme WPF.
  • L’asynchronisme et le parallélisme “naturel” de certains environnements comme IIS vis à vis de pages ASP.NET par exemple.

Bref dans tous ces cas il y exécution concurrente de code (mais pas forcément concurrence vis à vis des ressources communes).

Mais on pourrait étendre cette liste à bien d’autres situations, ce n’est qu’un aperçu de ce qui créée de la concurrence usuellement.

Asynchronisme et parallélisme

Blanc bonnet et bonnet blanc ? Pas tout à fait. Mais dans les faits cela reviendra à peu près au même du point de vue du développeur : des portions différentes de code vont tourner en même temps et il faudra le gérer.

Dans certains cas cela n’a pas d’importance comme une page ASP.NET accédée par cent utilisateurs en même temps. C’est IIS, .NET et l’OS qui gère la concurrence bien que dans certaines circonstances le développeur ait à prendre en compte la concurrence vis à vis des ressources si la page accède à une base de données ou un librairie ou ressource commune qui n’est pas thread safe.

Dans Silverlight, l’asynchronisme se manifeste dès qu’on appelle une fonction de communication, par un exemple un Web service ou des Ria Services. Dans un tel cas il n’y a pas vraiment concurrence, le code du thread principal continue à tourner et un appel distant est effectué, un autre code va tourner en même temps mais ailleurs, sur le serveur contacté, ce qui n’a aucun impact sur le code SL. En revanche la réponse arrivera n’importe quand et depuis le thread de communication. Si la réponse implique de manipuler l’UI il faudra s’assurer, via un Dispatcher en général, que cette manipulation s’opère bien sur le thread de l’UI et non celui de la communication.

La concurrence d’exécution ce sont plusieurs morceaux de codes qui tournent en même temps, la concurrence vis à vis des ressources (“race condition”) c’est quand ces morceaux de codes accèdent à des ressources communes, et l’asynchronisme c’est plutôt la pochette surprise, des évènements qui arrivent n’importe quand.

Quand on parle de parallélisme il y a bien entendu de l’asynchronisme le plus souvent, mais on souligne plutôt ici le caractère simultané du déroulement de plusieurs tâches ou bien le découpage d’une même tâche en plusieurs morceaux exécutés en même temps dans le but précis d’accélérer l’exécution ou de la rendre plus fluide.

L’asynchronisme est présent depuis toujours ou presque (le simple fait de demander à une imprimante si elle est prête et d’attendre la réponse sans bloquer l’interface utilisateur par exemple) alors que le parallélisme n’avait cours que dans les super ordinateurs. Ce n’est que depuis que les machines sont dotées de plus d’un cœur qu’on peut réellement parler de parallélisme sur un PC. Découper une tâche en plusieurs threads dans l’espoir que cela aille plus vite n’a pas de sens sur un processeur mono cœur alors qu’avec un multi-cœur cette stratégie sera terriblement payante.

Jusqu’à lors, le parallélisme n’existait donc pas, ou sous une forme “atténuée”, le multi-tâche qui longtemps fut juste simulé : le processeur passant rapidement d’une tâche à l’autre plusieurs fois par seconde donnant l’impression de la simultanéité alors qu’en réalité il n’exécute qu’une seule tâche à la fois.

Avec les machines multi-cœurs l’affaire devient toute autre car le multi-tâche devient parallèle, ce qui fait aussi apparaitre de l’asynchronisme.

Tout cela peut rapidement devenir complexe. On sait que la gestion du multi-tâche est de longue date réputée réservée aux “pointures” qui sont capables de manipuler les subtilités de ce mécanisme d’horlogerie sans se mélanger les pinceaux.

L’intérêt des librairies évoquées ici est de rendre plus simple l’implémentation de code sachant gérer le parallélisme et l’asynchronisme mais chacune avec une orientation différente :

  • Ainsi Task Parallel Library (TPL) est un plutôt une API moderne autour du ThreadPool qui permet de raisonner en termes de tâches plutôt que de threads. Le niveau d’abstraction atteint libère le développeur de certains détails pénibles et lui permet de mieux se concentrer sur ce qu’il cherche à faire (au lieu de comment le faire).
  • De son côté Asyn CTP est un peu comme ce qu’est Linq au traitement des données : une nouvelle syntaxe qui s’ajoute au langage pour rendre ici le traitement des tâches asynchrones plus simples et plus naturelles. Selon toute évidence Async CPT deviendra un élément de C# aussi indispensable que l’est devenu Linq.
  • Les Reactives extensions (Rx) ciblent autre chose. Ce sont plutôt des opérateurs de type Linq qui permettent de traiter des flux de données selon un mode consommateur. Tout devient flux de données, comme les évènements, ce qui permet d’écrire par exemple des opérations complètes comme un drag’n drop sous la forme d’une sorte de requête Linq. Un peu déroutant mais puissant.

 

Task Parallel Library (TPL)

La TPL a été releasée avec .NET 4.0 avec deux autres technologies qui tournent autour des mêmes problématiques :

  • Des structures de données améliorées pour la coordination (la très méconnue Barrier ou la ConcurrentQueue<T>)
  • Parallel Linq (PLINQ) qui est une extension de Linq construite sur la TPL offrant ainsi une syntaxe fluide autour de la gestion des flux de données.

TPL introduit un découplage plus fort entre ce qu’on veut faire (une tâche) et comment l’API le gère (un thread). Ainsi TPL nous offres des tâches pour raisonner et concevoir notre code en fonction de ce qu’il doit réaliser. On retrouve de fait des concepts de haut niveau plus simples à manipuler que la gestion du multithreading .NET usuel :

  • Des unités de travail ayant un cycle de vie bien défini (Created, Running...)
  • Des moyens simples d’attendre qu’une Task soit terminée ou qu’un groupe de Task le soit
  • Un moyen simple d’exprimer les dépendances mères/filles entre les tâches
  • L’annulation qui d’une option plus ou moins simple à mettre en œuvre devient un concept clé
  • La possibilité de construire des workflow conditionnels (la tâche 1 continue avec la tache 2 si la tâche 1 s’est bien terminée ou si elle a été annulée, etc.)

TPL permet aussi de planifier des tâches avec le TaskScheduler qui existe en plusieurs implémentations. La plus commune étant le ThreadPool avec ThreadPoolScheduler qui offre un ThreadPool fonctionnant avec des Tasks en optimisant ce traitement. Il est possible en partant de la classe abstraite TaskScheduler de créer son propre planificateur de tâches.

TPL est capable de gérer intelligemment le parallélisme en utilisant habilement les capacités de la machine hôte. Par exemple si TPL détecte que le CPU peut accepter une tâche de plus, une nouvelle sous-tâche est créée et exécutée automatiquement, si le CPU est déjà trop chargé, la sous-tâche est placée dans la file du thread en cours.

Ces capacités sont utilisées par PLINQ qui assure qu’une requête parallélisée le sera toujours au mieux de ce que peut supporter le CPU au moment de son exécution. C’est un progrès énorme si on pense au code qu’il faut écrire pour atteindre une telle souplesse.

TPL offre aussi la TaskFactory<T> avec des méthodes comme FromAsync() qui permettent de faire le lien entre l’ancienne modèle de programmation asynchrone et le nouveau monde des tâches.

Il devient ainsi possible d’écrire un code comme celui-ci basé sur un appel asynchrone HTTP GET :

   1: Task parentTask = new Task(
   2:   () => 
   3:   { 
   4:     WebRequest webRequest = WebRequest.Create("http://www.microsoft.com"); 
   5:     
   6:     Task<WebResponse> task = 
   7:       Task<WebResponse>.Factory.FromAsync(
   8:         webRequest.BeginGetResponse, webRequest.EndGetResponse, 
   9:         TaskCreationOptions.AttachedToParent); 
  10:     
  11:     task.ContinueWith(
  12:       tr => 
  13:       { 
  14:         using (Stream stream = tr.Result.GetResponseStream()) 
  15:         { 
  16:           using (StreamReader reader = new StreamReader(stream)) 
  17:           { 
  18:             string content = reader.ReadToEnd(); 
  19:             Console.WriteLine(content); 
  20:             reader.Close(); 
  21:           } 
  22:           stream.Close(); 
  23:         } 
  24:       }, TaskContinuationOptions.AttachedToParent); 
  25:   }); 
  26:  
  27: parentTask.RunSynchronously(); 
  28: Console.WriteLine("Done"); 
  29: Console.ReadLine();

 

Une tâche “parent” est créée décrivant à la fois la requête asynchrone HTTP ainsi qu’une tâche fille, liée à la première, se chargeant de gérer la réponse.

Le code effectue un parentTask.RunSynchronously(), ce qui signifie que les deux tâches asynchrones vont être ainsi contrôlée et contraintes dans un appel qui lui est bloquant, simplifiant énormément l’écriture du code. L’écriture de “Done” à la console est placée à la ligne suivante et ne sera exécuté que lorsque que l’ensemble de la tâche et ses sous-tâches seront terminées.

On retrouve ici un peu l’esprit des coroutines exploitées par la gestion des Workflows de Jounce mais techniquement cela est très différent (Le Workflow Jounce garantit l’exécution séquentielle de plusieurs tâches mais qui n’est pas bloquant au niveau de l’exécution du Workflow lui-même, TPL offrant ainsi plus de confort).

Il ne s’agit pas dans ce billet de faire un cours détaillé de TPL mais d’expliquer les nuances entre les trois technologies présentées en introduction. Je pense que vous avez compris ce que TPL fait, ce qu’il offre comme avantages principaux. J’y reviendrai certainement plus en détail dans de prochains billets (surtout Parallel LINQ).

Async CTP

Async CTP est une nouvelle technologie qui appartient en réalité à C# 5, elle n’existe donc qu’en l’état de bêta pour les tests. Pas de production avec Async CTP pour le moment, mais prochainement.

TPL est une avancée intéressante mais transitoire. TPL sera certainement plus utilisée au travers de Parallel LINQ que directement car Async CTP qui sera intégré à C# 5 rendra son utilisation presque caduque.

En effet, TPL oblige à une certaine gymnastique pas toujours évidente dès qu’on souhaite synchroniser plusieurs tâches qui s’enchainent. L’écriture du code devient fastidieuse car trop mécanique tout en réclamant un bon niveau de concentration pour ne pas faire de bêtise.

Async CTP ajoute des éléments au langage C#, ces éléments savent déclencher la génération automatique du code fastidieux, rendant le code utilisateur bien plus clair, plus fluide et évitant de l’encombrer de portions très mécaniques mais essentielles.

Par exemple il devient possible d’écrire une méthode qui retourne une Task ou Task<T> plutôt qu’un résultat standard. L’appelant de cette méthode peut alors attendre l’exécution de la tâche asynchrone plutôt que de recevoir un résultat immédiat. Une fonction peut aussi présenter de “faux” multiples points de retour (“await”) que le compilateur va restructurer en une série de callbacks de façon totalement transparente pour le développeur.

Un code utilisant Async CTP et réalisant la même chose que le code précédent ressemblerait à cela :

   1: Func<string, Task> task = async (url) => 
   2: { 
   3:   WebRequest request = WebRequest.Create(url); 
   4:   Task<WebResponse> responseTask = request.GetResponseAsync(); 
   5:   await responseTask; 
   6:   
   7:   Stream responseStream = responseTask.Result.GetResponseStream(); 
   8:   Stream consoleStream = Console.OpenStandardOutput(); 
   9:   
  10:   byte[] buffer = new byte[24]; 
  11:   
  12:   Task<int> readTask = responseStream.ReadAsync(buffer, 0, buffer.Length); 
  13:   await readTask; 
  14:   
  15:   while (readTask.Result > 0) 
  16:   { 
  17:     await consoleStream.WriteAsync(buffer, 0, readTask.Result); 
  18:     readTask = responseStream.ReadAsync(buffer, 0, buffer.Length); 
  19:     await readTask; 
  20:   } 
  21:   responseStream.Close(); 
  22: }; 
  23:  
  24: task("http://www.microsoft.com").Wait(); 
  25:  
  26: Console.WriteLine("Done"); 
  27: Console.ReadLine(); 

Ici tout est asynchrone, la lecture et l’écriture du résultat, mais le code est court, lisible, l’intention est plus flagrante :

  • Une requête HTTP GET est créée
  • On attend la réponse asynchrone
  • On obtient le flux réponse qui est écrit de façon asynchrone à la console
  • On boucle de façon entre la lecture et l’écriture par paquet de 24 octets (la taille du buffer déclaré)

 

Aync CTP c’est cela : une amélioration très nette de C# afin de prendre en compte l’asynchronisme de façon naturelle.

Il s’agit d’une étape aussi essentielle que l’ajout de LINQ qui intégrait au langage la gestion des données.

L’informatique moderne gère essentiellement des données et doit aujourd’hui prendre en compte le parallélisme. C# s’inscrit au fil du temps dans une modernité constante, intégrant naturellement des éléments qui dépassent de loin les traditionnels “if then else” des langages classiques qui laissent au développeur toute la responsabilité de trier ou filtrer des données et d'orchestrer manuellement le ballet fragile du multitâche.

Ici aussi le but n’est pas de faire un cours sur Async CTP mais juste de vous faire comprendre à quoi cela peut servir et dans quel contexte. J’y reviendrai forcément, c’est une partie importante des nouveautés de C# 5.

Les Rx

Avec Async CTP, TPL et Parallel LINQ, on se demande quelle place peut bien rester vide pour qu’une autre librairie puisse venir s’y loger...

Asynchronisme, parallélisme, multitâche, traitement des données, tout cela peut se tisser en une trame si complexe et si différente d’une application à l’autre qu’il existe encore beaucoup de place pour autre chose. Les Reactive extensions.

Les Rx ne se concentrent pas forcément sur le parallélisme ou l’asynchronisme mais plutôt sur la façon de gérer simplement des séquences de valeurs qui sont générées dans le temps, peu importe les délais entre les moments où ces valeurs sont créées.

Bien entendu derrière tout cela on entend bien le son de l’asynchronisme comme on entend celui des timbales scander le rythme derrière un orchestre symphonique. Les Rx suppose une gestion fine de l’asynchronisme, transparente. Tellement transparente qu’elle n’est plus l’objectif premier, la gestion de l’asynchronisme n’est plus que l’assise permettant la gestion de flux de données.

Les Rx sont bâties sur toutes les notions que nous avons vues plus haut. Mais ce ne sont que des briques de construction. La finalité des Rx n’est pas de gérer directement du parallélisme ou de l’asynchronisme. Elles permettent juste de les prendre en compte de façon transparente pour accomplir quelque chose de plus sophistiqué.

La composition de séquences de valeurs est a entendre au sens le plus large avec les Rx, des séquences de prix d’articles sont tout aussi bien utilisables que des séquences de nouveaux items arrivant dans une collection ou même qu’une séquence d’évènements souris ou clavier...

Les Rx proposent un ensemble d’opérateurs qu’on peut voir comme une sorte de DSL pour traiter des séquences de valeurs.

(un DSL est un Domain Specific Language, un langage spécifiquement adapté à un type de tâche bien précis, à la différence d’un langage classique se voulant générique).

Les Rx permettent ainsi des opérations de type :

  • Création ou génération de séquences
  • Combinaison de séquences
  • Requêtage, projection et filtrage de séquences
  • Groupage et tris de séquences
  • Altération de la nature temporelle des séquences en y intégrant des attentes, des délais, des bufferisations...

 

Le cœur des Rx est IObservable<T>, une collection bien particulière qui permet de gérer les séquences de valeurs.

Partant de cette collection particulière et avec l’ajout d’opérateurs Linq spéciaux, il est possible de créer des requêtes de type Linq jouant non plus sur des listes pré-existantes de valeurs mais sur des flots asynchrones de données qui n’existent pas encore.

Imaginons trois méthodes dont l’exécution est assez longue et pouvant même dépendre de données totalement asynchrones (des clics souris, des données en mode push...) :

   1: int TaskA()
   2: {    
   3:    Thread.Sleep(200);    
   4:    return 42;
   5: }
   6:  
   7: string TaskB()
   8: {
   9:    Thread.Sleep(500);
  10:    return "La réponse est {0} ! {1}";
  11: }
  12:  
  13: string TaskC(){ return "Incroyable !";}

le but du jeu est d’exécuter ces trois méthodes indépendamment, de collecter les résultats et d’en produire une information finale qui sera écrite à l’écran quels que soient les délais d’attente qui peuvent fort bien être très différents de l’ordre dans lequel il faut obtenir les résultats pour accomplir le travail.

(l’exemple utilise des Thread.Sleep() pour simplifier mais ce n’est pas à reproduire, ne prenez pas cela au pied de la lettre)

Avec du code .NET classique cela donnerait ça :

   1: var waitHandles = new List<WaitHandle>();
   2: int ARet = 0;
   3: Func<int> A = TaskA;
   4: var ARes = A.BeginInvoke(res => { ARet = A.EndInvoke(res); },null);
   5: waitHandles.Add(ARes.AsyncWaitHandle);
   6: string BRet = "";
   7: Func<string> B = TaskB;
   8: var BRes = B.BeginInvoke(res => { BRet = B.EndInvoke(res); }, null);
   9: waitHandles.Add(BRes.AsyncWaitHandle);
  10: string CRet = "";
  11: Func<string> C = TaskC;
  12: var CRes = C.BeginInvoke(res => { CRet = C.EndInvoke(res); }, null);
  13: waitHandles.Add(CRes.AsyncWaitHandle);
  14: WaitHandle.WaitAll(waitHandles.ToArray());
  15: Console.Out.WriteLine(ARet, BRet, CRet);

Les méthodes sont exécutées dans un ordre précis, avec une attente de chaque résultat avant de passer à l’exécution de la méthode suivante.

C’est assez indigeste, pas vraiment agile, l’intention initiale est noyée dans la technique pour exécuter la tâche au lieu que cette dernière soit clairement identifiable.

Avec les Rx on peut écrire le même code de la façon suivante :

   1: Observable.Join(
   2:     Observable.ToAsync<int>(TaskA)()
   3:         .And(Observable.ToAsync<string>(TaskB)())
   4:         .And(Observable.ToAsync<string>(TaskC)())
   5:         .Then((a, b, b) =>
   6:              new { A = a, B = b, C = c })
   7:     ).Subscribe(
   8:         o => Console.WriteLine(o.A, o.B, o.C),
   9:         e => Console.WriteLine("Exception: {0}", e));

Mais on pourrait vouloir exécuter tout cela de façon réellement asynchrone (exécution parallèle des méthodes). Avec du code classique cela serait très pénible. Avec les Rx il suffit d’écrire :

   1: (from a in Observable.ToAsync<int>(TaskA)()
   2:  from b in Observable.ToAsync<string>(TaskB)()
   3:  from c in Observable.ToAsync<string>(TaskC)()
   4:  select new { A = a, B = b, C = c })
   5:  .Subscribe(o => Console.WriteLine(o.A, o.B, o.C));

C’est encore plus court !

Les Rx propose ainsi une nouvelle façon de penser le traitement d’évènements asynchrones en les séquentialisant au sein d’une syntaxe logique, claire, déclarative, qui plus est réexploitant la puissance de LINQ.

Les Rx ont ainsi toute leur place à côté de Async CTP et de la TPL.

Conclusion

Il n’était pas question ici de faire un cours complet sur chaque des technologies présentées mais plutôt de vous aider à comprendre à quoi elles correspondent, qu’en attendre et vous faire découvrir ce nouveau monde parallèle et asynchrone. A la clé, vous donnez envie d’en savoir plus et de tester par vous-mêmes !

TPL avec Parallel LINQ est déjà intégré au Framework .NET 4.0. C’est dans la boite, vous pouvez vous en servir dès maintenant.

Async CTP est un CTP, un simple preview d’une technologie qui fait partie de la prochaine version 5 de C#. Vous pouvez installer la bêta de test depuis la galerie Visual Studio (Microsoft Visual Studio Async CTP).

Quant aux Rx elles existent en version 1.0 stable et peuvent être téléchargées sur le Data Developer Center de MS (Reactive Extensions).

Trois technologies qui semblent similaires mais qui, vous le voyez maintenant, offrent un angle de pénétration dans le monde de l’asynchronisme totalement différent. Chacune a son intérêt, sa place. Il faut juste s’y former, comprendre la nouvelle façon de concevoir le code.

Une autre histoire !

Je reviendrai sur ces technologies essentielles dans de prochains billets. Mais que cela ne vous empêche pas de vous y intéresser par vous même !

... Stay Tuned !

blog comments powered by Disqus