La programmation parallèle en C#

Mis à jour le 11/11/2024

Cédric BRASSEUR

  • Ancien étudiant de l'Exia.cesi
  • 4 ans consultant chez Amaris, placé chez Euro Information Dev
  • Auto-entrepreneur depuis début 2020 (Création de sites web, applications mobiles & formateur)

Et vous ?

  • Nom, prénom
  • Etudes réalisées 
  • Connaissances en dév
  • Autres infos appréciées

Plan du cours

  • Introduction à la théorie de la programmation parallèle
  • Explications des classes de PP in C#
    • Task
    • Parallel
    • PLinq
    • ...
  • Démonstration et exemples
  • Exercices de PP in C#
    • Exercices simples d'utilisation
    • Exercices en PLinq
    • Exercice de mise en place d'une API
    • Exercice complet de transformation d'un code synchrone en code asynchrone

// in C#

Objectifs de la formation

  • Appréhender la notion de programmation parallèle

  • Concepts de la prog. parallèle

  • Exemple et démonstration

  • Exercices applicatifs pour s'approprier la notion

Théorie et programmation parallèle

Utilité de la PP

La programmation parallèle a pour objectif de faire tourner du code sur plusieurs processeurs (ou plusieurs cœurs). Ceci permettant de réaliser des opérations en parallèle et donc dans certains cas optimiser le temps pour de lourds traitements.

Processeurs

Utilité de la PP

L'objectif est donc de diviser un problème en plusieurs parties et faire s'exécuter des bouts de solutions de manière parallèle afin d'optimiser le temps de traitement. 

A noter que c'est utilisable dans certains contexte mais que dans d'autres, ça peut être s'ajouter de la complexité pour peu de gains.

Quand utiliser la Programmation Parallèle ?

Il y a plusieurs cas d'usages récurrents pour la PP :

  • Lorsque l'on souhaite mettre en place une interface (UI) et que nous souhaitons laisser l'affichage global s'effectuer et réaliser les opérations annexes en background
  • Lors de la mise en place d'une API : Double utilité, d'abord pour traiter les demandes de clients différents & pour pouvoir effectuer plusieurs requêtes en simultanées
  • Lors d'opérations couteuses que l'on peut diviser en plusieurs petites parties indépendantes

Types de PP

Task-bases Asynchronous Pattern (TAP)

Event-bases Asynchronous Pattern (EAP)

Asynchronous Programming Model (APM)

On utilise une seule méthode pour compléter une suite de tâche asynchrone. C'est le type recommandé en .Net et nous nous attarderons sur ce type.

On utilise des events pour produire des comportements asynchrone. C'est en quelques sortes l'ancienne méthode donc nous l'aborderons pas.

Mise en place de méthodes avec Begin & End et une interface IAsyncResult pour traiter des opérations asynchrones. C'est une façon de faire qui n'est plus recommandé du tout aujourd'hui.

.Net 1.1

.Net 2.0

.Net 4.5

Threads VS Process

Process Threads
Opérations lourdes Opérations plus légères
A son propre espace mémoire Partage la mémoire du process auquel ils appartiennent
Communication inter-process lente et adresse mémoire différentes D'où la communication plus rapide entre threads
Passer d'un process à l'autre est coûteux Passer d'un thread à l'autre est moins coûteux
Aucun partage de mémoire Partage de la mémoire

Un process est une unité de code qui permet de réaliser des opérations (instructions). On peut voir les Threads comme des micro-process qui peuvent communiquer entre eux et utiliser une mémoire commune

Threads en détails

Juste à titre informatif, voici comment fonctionnent les Threads 

  • Register : C'est l'unité de travail qui contient les données nécessaire au process (adresse de stockage,...)
  • Program Counter : Le pointeur d'instruction, il joue un rôle organisationnel et garde une trace de qui travaille sur quoi.
  • Stack : Une structure de donnée qui stock les données d'un programme. C'est à distinguer de la mémoire dynamique, on l'appelle "Heap"
  • Thread Scheduler : C'est l'OS qui défini quel process s'exécutera sur quel processeur

Synchrone VS Asynchrone

Ces termes doivent déjà vous parler mais il s'agit de programmation concurrentielle & de programmation parallèle. 

Concurrentielle

Parallèle

CPU

Unité de travail

CPU 1 / Thread

Unité de travail

CPU 2 / Thread

Unité de travail

Parallèle

Unité de travail

Unité de travail

Asynchrone

=

Parallèle + interdépendances

Synchrone exemple

Quelques informations sur un programme qui utilise les process : Google Chrome.

Comme on peut le voir dans le Task Manager, Chrome utilise plusieurs processus, d'ailleurs il en met en place parfois plusieurs pour chaque onglet. Ceci apportant plusieurs avantages :

  • Protège des bugs qui peuvent impacter les autres onglets.
  • Isole le code (Javascript par exemple) permet d'éviter d'utiliser trop de CPU et de mémoire en une seule fois

Asynchrone exemple

Voici un exemple que j'utiliserai afin de vous montrer comment faire de la programmation parallèle (Exemple de Microsoft)

Concurrentielle

Asynchrone

Aller plus loin dans la théorie

Bien que ça ne soit pas vraiment au programme de ce cours, je sais que ça en intéressera plus d'un (une) d'approfondir la théorie de la PP.

Quelques termes pour guider vos recherches

  • Loi de Moore
  • Cores (et puces avec plusieurs cœurs)
  • SIMD (single instruction multiple data)
  • Thread (méthode) / Process (module)
  • Green thread (Fiber) / Kernel thread
  • ILSpy (outil d'analyse)
  • Coroutine
  • Sémaphore
  • Mutex
  • Data Race
  • Deadlocks
    ...

Des gens intéressés pour faire une petite présentation de la théorie ?

Les classes et méthodes en C# pour la programmation parallèle

Les classes de PP en C#

Nous allons voir plusieurs classes disponibles dans le Framework .Net afin de réaliser de la programmation parallèle en C# de nos jours (TAP) :

  • Les mots clés async & await
  • Task
  • Parallel
  • Action
  • PLinq

Premièrement, le mot clé async permet de définir une méthode comme étant asynchrone.
Ensuite, le mot clé await permet de définir que l'on attend le retour d'une méthode asynchrone

Les mots clés await / aync

On peut avoir une méthode async en void, mais ceci nous forcerait à ne pas pouvoir attendre le retour, donc nous ne serions pas sûr de la fin de l'exécution de la méthode.

Si on déclare une méthode comme asynchrone, il doit y avoir un await dedans

Nous utilisons
également les méthodes asynchrones d'appels d'API ici 

Les mots clés await / aync

Ces mots clés sont toujours utilisé en programmation parallèle, ils permettent de définir une méthode comme asychrone (async) et d'attendre le retour (await)

S'il on déclare une méthode comme asynchrone, il doit y avoir un await dedans

Nous utilisons
également les méthodes asynchrones d'appels d'API ici 

On utilise Task en type de retour car il n'est pas possible d'attendre sur void

La classe Task

La classe Task a pour but de vous permettre de gérer vos appels asynchrones en C#, c'est la plus utilisée.

Voici les différentes possibilités que l'on va explorer :

Class Objectif
Task Gérer une unité de travail
Task<TResult> Unité de travail avec une valeur de retour
TaskFactory Créer une Task
TaskFactory<TResult> Créer une Task & continuité de Task avec le même type de retour
TaskCompletionSource Pour gérer manuellement le workflow d'une Task

Premier exemple simple d'utilisation d'une Task :

On déclare le type de retour en Task afin de pouvoir l'await.

On utilise Task.WhenAll()
pour attendre que toutes les opérations soient
terminées

La classe Task

Task.WhenAll respecte l'ordre de passage des tasks dans le retour de result[] (même si les temps d'exécution sont différents)

Voici les méthodes que nous allons utiliser pour réaliser des opérations parallèles

On fait en sorte que
le thread attende 2 secondes pour simuler une tâche lourde

La classe Task

Attention, il y a plusieurs moyens d'appeler une Task, nous en verrons cinq. Dont trois seulement sont optimisés, ne faites pas l'erreur, sinon l'asynchronisme ne sert à rien.

On déclare le type de retour
en Task<string> afin de pouvoir l'await avec retour

Chaque tâche est attendue l'une après l'autre

Ce moyen attend les tâches l'une après l'autre et prend donc 6 secondes

La classe Task

Attention, il y a plusieurs moyens d'appeler une Task, nous en verrons cinq. Dont deux seulement sont optimisés, ne faites pas l'erreur, sinon l'asynchronisme ne sert à rien.

SendEmailAsync n'ayant
pas de type de retour, on peut l'appeler ainsi également

Ce moyen attend les tâches l'une après l'autre, sauf SendEmailAync et prend donc 4 secondes

Seul cet appel sans type de
retour sera réalisé en même temps que les autres

La classe Task

Attention, il y a plusieurs moyens d'appeler une Task, nous en verrons cinq. Dont deux seulement sont optimisés, ne faites pas l'erreur, sinon l'asynchronisme ne sert à rien.

On créer une Task
pour chaque méthode à appeler

Ce moyen réalise les tâches en parallèle, ceci prend donc 2 secondes

Les appels sont fait de manière asynchrone

La classe Task

Attention, il y a plusieurs moyens d'appeler une Task, nous en verrons cinq. Dont deux seulement sont optimisés, ne faites pas l'erreur, sinon l'asynchronisme ne sert à rien.

On créer une Task
pour chaque méthode à appeler

Ce moyen réalise les tâches en parallèle, ceci prend donc 2 secondes

On utilise Task.WhenAll
pour attendre les actions en parallèle

Clean !

La classe Task

Attention, il y a plusieurs moyens d'appeler une Task, nous en verrons cinq. Dont deux seulement sont optimisés, ne faites pas l'erreur, sinon l'asynchronisme ne sert à rien.

On créer une Task
pour chaque méthode à appeler

On créer une List de Tasks

On boucle sur la liste de tasks

Ce moyen prend également 2 secondes mais permet d'avoir la main sur les Tasks

On boucle sur la liste de tasks

On boucle sur la liste de tasks

Dès qu'une tâche est terminée, on la supprime avec Remove

La classe Task

Exercice class Task

Afin que vous pratiquiez un peu l'utilisation de la classe Task, vous devez réutiliser les exemples précédents et réaliser différents appels asynchrones pour pratiquer.
Le but est de reprendre le code et le comprendre :

  • Le mot clé async / await avec deux requêtes vers 
    • https://jsonplaceholder.typicode.com/todos/1
    • https://jsonplaceholder.typicode.com/todos/2
    • Et afficher le contenu json dans un seul Console.WriteLine
  • La classe Task 
    • Je vous envoie le code FakeAsyncTask et à vous de jouer

Posez des questions si des choses ne sont pas claires pour vous

Il est également possible d'utiliser une Factory pour créer une Task et la démarrer :

On peut également mettre
un appel à une méthode ici

Voici un exemple un peu plus complet qui permet de réaliser des opérations de manière parallèle ainsi que les ajouter à une somme totale et l'afficher.
Par contre impossible de faire des opérations asynchrones dans DoComputation

La classe Task

Il est également possible d'utiliser Run afin de réaliser des opérations similaires avec possibilité d'asynchronisme (recommandé)

Exemple avec Task.Run

La classe Task

Task permet également d'utiliser ContinueWith afin de pouvoir continuer un traitement asynchrone en s'assurant d'avoir l'ensemble des résultats attendus avant de continuer le code tout en gérant efficacement les exceptions.

Exemple ContinueWith

La classe Task

ContinueWith permet également de gérer les exceptions (il est d'ailleurs conseiller de gérer les différents statuts de retour). Voici un petit exemple

Exemple ContinueWith

La classe Task

Exercice class Task

Un petit exercice d'application du ContinueWith. Il vous est demandé de réalisé le code (via une Task<string>) permettant de gérer le retour de cette méthode async d'appel à une api quelconque.
Vous devez gérer les différents status (RanToCompletion & Faulted).
Pensez à tester avec une api qui n'existe pas (enlevez le i à official par exemple)

Posez des questions si des choses ne sont pas claires pour vous

static async Task<string> FetchDataAsync()
{
    using (HttpClient httpClient = new HttpClient())
    {
        HttpResponseMessage response = await httpClient.GetAsync("https://official-joke-api.appspot.com/random_joke");
        response.EnsureSuccessStatusCode(); // Permet de lever une exception si l'appel n'a pas renvoyé une réponse valable

        return await response.Content.ReadAsStringAsync();
    }
}

Autre exercice class Task

Le but de cet exercice est de travailler encore une fois la classe Task sur un exemple légèrement plus complexe et moins guidé.

Vous avez le code de la méthode "DoWork". Votre objectif est d'appeler trois fois cette méthode avec les paramètres suivants :

  • "Task 1", 3000
  • "Task2", 2000
  • "Task3", 4000

Bien entendu vous devez appelez les trois méthodes de manière asynchrone.

Pour terminer, vous devez afficher le cumul des 3 délais retournés PUIS ajouter 500 au cumul pour l'afficher.

static int DoWork(string taskName, int delayMilliseconds)
{
    Console.WriteLine("{0} starting...", taskName);
    Task.Delay(delayMilliseconds).Wait();
    Console.WriteLine("{0} completed.", taskName);
    return delayMilliseconds;
}

La classe Parallel

Comme on l'a vu, la classe Task a pour objectif de réaliser une opération de manière asynchrone, souvent des opérations lourdes. Une autre possibilité pour la PP est d'utiliser la classe Parallel. Cette dernier va vous permettre d'effectuer un nombre de fois des opérations en parallèle. L'objectif est de simplifier la réalisation d'actions en parallèle.
A noter qu'on utilise Parallel lorsqu'il n'y a pas d'interdépendances entre les éléments que l'on veut rendre asynchrone.

Attention, ce moyen de paralléliser les instructions ne permet pas de garder l'ordre de départ de la liste.

Il est possible de limiter le nombre de processeurs utilisés lors de l'utilisation de la classe Parallel. Ceci se fait avec des options de la classe ParallelOptions.

Il existe aussi des options pour mettre en place un token d'annulation (CancellationToken) & pour choisir le planificateur de tâche (TaskScheduler). On l'a entrevu normalement, mais c'est aussi possible avec Task (et des constructeurs différents)

A noter, ceci est très rarement utile, surtout qu'il se peut que d'une machine à l'autre ces éléments varient.

La classe Parallel

Un autre exemple de code et une autre façon de pouvoir utiliser la classe Parallel. Notez dans l'exemple du dessous que nous utilisons une aggrégation de résultat.

Attention, ce moyen de paralléliser les instructions ne permet pas de garder l'ordre de départ de la liste.

La classe Parallel

Un autre exemple de code et une autre façon de pouvoir utiliser la classe Parallel

Attention, ce moyen de paralléliser les instructions ne permet pas de garder l'ordre des lignes provenant d'un fichier

La classe Parallel

Un autre exemple de code et une autre façon de pouvoir utiliser la classe Parallel, mais cette fois-ci avec Parallel.Invoke et en modifiant le retour (Upper / Lower)

Attention, ce moyen de paralléliser les instructions ne permet pas de garder l'ordre des lignes provenant d'un fichier

La classe Parallel

Dans ce contexte, nous ne travaillons donc plus avec des Task, mais avec des Action.
Chaque action peut être effectuée en parallèle et être appelé de manière asynchrone grâce à la Parallel.Invoke.

Ce code mettra à peine plus de 1,5 secondes à s'exécuter car les actions sont déclenchées en parallèle.

La classe Parallel

Exercice class Parralel

Une partie de ces exercices sont aussi présents dans les slides, mais ils ont été légèrement adapté pour un peu plus vous forcer à réfléchir durant la pratique.

Posez des questions si des choses ne sont pas claires pour vous

Envoyer Exercice1_PP.md

+ file1.txt
+ file2.txt

PLinq (Parallel with Linq)

Un dernier moyen existe afin de réaliser des opérations parallèles sur des listes d'éléments. Nous pouvons faire du PLinq pour réaliser du parallélisme avec Linq (mais c'est peu recommandé en règle général)

On utilise la méthode
AsParallel() sur une liste ou un tableau afin de rendre les itérations asynchrones

Ceci permet d'afficher les éléments de la liste de manière asynchrone, attention ici aussi ça ne garde pas l'ordre de départ de la liste

On peut utiliser la
méthode ForAll() sur le retour de AsParallel().Where()

PLinq (Parallel with Linq)

Voici un exemple de code ou on utilise PLinq mais on force le fait de respecter l'ordre de la liste de départ avec AsOrdered() puis un nouveau ForEach() dessus.

On ajoute la méthode
AsOrdered() pour garder l'ordre des entrées, mais c'est pas tout

Ici, la liste de départ est respectée. A noter que si vous êtes en .Net Framework < 7, la durée d'exécution sera plus longue sur ce cas. Sinon elle sera plus rapide car il y a eu de grosses optimisations en Framework 7.0 sur le PLinq

On doit retransformer le retour de AsParallel().AsOrdered().
Puis, on transforme en liste, puis on fait un ForEach

PLinq (Parallel with Linq)

Voici un autre exemple de code un peu plus complet pour l'utilisation de PLinq.

De nouveau, ici la l'ordre de la liste de départ n'est pas respecté.

PLinq (Parallel with Linq)

Encore un autre exemple de code où ici on combine PLinq et l'utilisation de Task

De nouveau, ici la l'ordre de la liste de départ n'est pas respecté.

On utilise la méthode
ContinueWith() sur la liste
pour effectuer une opération
asynchrone

Nous sommes contraint
de réaliser un foreach classique afin de pouvoir se référer à task.Result 

Exercice class PLinq

Une partie de ces exercices sont aussi présents dans les slides, mais ils ont été légèrement adapté pour un peu plus vous forcer à réfléchir durant la pratique.

Posez des questions si des choses ne sont pas claires pour vous

Envoyer Exercice2_PP_PLinq.md

Apparté Exception & parallélisme

Il est important de savoir que la première exception levée stoppera l'exécution de votre code si vous utilisez les Exceptions classiques dans votre code.

Ici, nous simulons les exception (tests) mais comme ça, seule la première exception est récupérée

Apparté Exception & parallélisme

Il est important de savoir que la première exception levée stoppera l'exécution de votre code si vous utilisez les Exceptions classiques dans votre code.

En créant votre propre classe d'exception, vous pouvez récupérer toutes les exceptions pour toutes les tâches

TaskCompletionSource

Honnêtement, je n'utilise pas trop cette classe, mais autant la voir rapidement pour en voir un exemple (sans faire d'exo)

Une partie ou l'on génère un résultat

TaskCompletionSource

Honnêtement, je n'utilise pas trop cette classe, mais autant la voir rapidement pour en voir un exemple (sans faire d'exo)

Une partie ou l'on génère une exception intentionnellement

Exercices plus complexe de programmation parallèle

Petit tutoriel sympa à suivre si on a le temps...

Exercice plus complet avec la préparation d'un petit déjeuné

Concrètement, je vais vous envoyer le code non asynchrone, vous allez devoir le rendre asynchrone, selon les captures ci-desous.

Conseil pratique : Vous pouvez utiliser des Tasks et utiliser WhenAny() pour bien implémenter tout ça

Corrigé de l'exercice avec la préparation d'un petit déjeuné

L'évaluation ressemblera assez à ce type de traitements parallèles, donc je vous enverrai le corrigé et prenez le temps de l'analyser / poser des questions

Mise en place d'une API en utilisant la programmation Parallèle

Pourquoi faire une API en asynchrone ?

Il existe plusieurs avantages à mettre en place une API de manière asynchrone : 

  • Permettre de réaliser des appels en parallèle depuis un code client (Nous allons créer une API et utiliser un projet Console pour remplir les informations d'une formation pour voir un exemple simple)
  • Permettre au serveur contenant l'API de travailler sur plusieurs processeurs avec des process multiples (Ceci peut donc surcharger le CPU du serveur, mais peut aussi grandement améliorer les performances de votre API)
  • Aujourd'hui, en .Net toutes les API sont asynchrones, ou alors elles permettent de réaliser les deux.

Mettre en place une API en .Net

Il existe plusieurs façons de mettre en place une API en .Net, nous allons en utiliser une assez simple en utilisant FastEndpoint. (A ma connaissance, il faut avoir dans outils => obtenir les outils et fonctionnalités => Développement web et ASP.Net)

Petite démonstration rapide de comment mettre en place une API avec FastEndpoint + Parallélisation des appels

Documentation FastEndpoint (si besoin)

Mettre en place une API en .Net

Nous allons réaliser les étapes ensemble pour un simple test, puis vous aurez un exercice à réaliser qui s'en rapproche afin de vraiment tester tout ça.

Etapes à suivre :

  • Créer un nouveau projet ASP.Net Web Application (nommez le "SocialsEndpoint"
  • Ajoutez le package Nuget "FastEndpoint"
  • Cleaner le Program.cs en le remplaçant par ce qui suit :
using FastEndpoints;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFastEndpoints();

var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseFastEndpoints();
app.Run();

Mettre en place une API en .Net

Nous allons réaliser les étapes ensemble pour un simple test, puis vous aurez un exercice à réaliser qui s'en rapproche afin de vraiment tester tout ça.

Etapes à suivre :

  • Modifier le appsettings.json en rajoutant le noeud Kestrel comme suit :
{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "Https": {
        "Url": "https://localhost:5001"
      }
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Mettre en place une API en .Net

Nous allons réaliser les étapes ensemble pour un simple test, puis vous aurez un exercice à réaliser qui s'en rapproche afin de vraiment tester tout ça.

Etapes à suivre :

  • Créer un dossier "Controller" puis ajoutez une classe vide cette classe hériter de "EndpointWithoutRequest" et doit override deux méthodes Configure & HandleAsync(CancellationToken ct) 
 public class EndpointTest : EndpointWithoutRequest
 {
   public override void Configure()
   {
     Get("api/test");
     AllowAnonymous();
   }

  public override async Task HandleAsync(CancellationToken tc)
  {
    await Task.Delay(1000);
    await SendAsync(new
    {
    	test = "test"
    }, cancellation: tc);
  }
}

Mettre en place une API en .Net

Nous allons réaliser les étapes ensemble pour un simple test, puis vous aurez un exercice à réaliser qui s'en rapproche afin de vraiment tester tout ça.

Etapes à suivre :

  • Démarrer votre projet web ! Votre API doit fonctionner à cette étape (vous pouvez tester via postman (https://localhost:5001/api/test doit retourner "test")
  • Il ne reste plus qu'à faire l'appel depuis le projet Console (un nouveau ou un utilisé auparavant)
  • (Et une étape à la slide suivante côté projet Console pour GetTest(client))
// code in Program.cs
HttpClient client = new HttpClient();
var testTask = ApiAsyncMethods.GetTest(client);
var testResult = await testTask;
Console.WriteLine("Test result value {0}", testResult);

Attention, j'ai fait
exprès ici de ne pas vous donner les lignes de code pour l'asynchrone

Mettre en place une API en .Net

Nous allons réaliser les étapes ensemble pour un simple test, puis vous aurez un exercice à réaliser qui s'en rapproche afin de vraiment tester tout ça.

Etapes à suivre :

  • Donc dernière étape : L'appel asynchrone et le mapping des données dans une classe, peu importe, pour l'exemple, j'ai une classe static avec une méthode static
internal class TestFastEndpoint
{
	public string test { get; set; }
}

public static async Task<string> GetTest(HttpClient client)
{
  HttpResponseMessage response = await client.GetAsync("https://localhost:5001/api/test");
  var jsonContent = await response.Content.ReadAsStringAsync();
  var test = JsonSerializer.Deserialize<TestFastEndpoint>(jsonContent);

  return test.test; // Oui, c'est pas très beau mais c'est un test !
}

Mettre en place une API en .Net

Exercice

  • Pour cet exercice, vous allez devoir implémenter 3 nouveaux endpoints
    • FacebookFollowers (retour un nombre en dur de followers en entier)
    • InstagramFollowers (de même)
    • TwitterFollowers (de même)
  • Votre objectif est de faire 3 méthodes contenant un retour de followers, chacun des méthodes doit faire un await Task.Delay(1000); comme vu dans l'exemple test. Et retourner une valeur en dur d'entier dans HandleAsync via SendAsync()
  • Côté projet Console, votre but est de remplir les informations d'une classe "User" (Name (setté par le constructeur), FacebookFollowers, TwitterFollowers, InstagramFollowers) en utilisant la programmation asynchrone (Task.WhenAll par exemple) pour réaliser l'opération de remplissage des infos de l'utilisateurs en 1 seconde (et non 3). Il vous faut donc un timer
  • Vous devez afficher les informations de l'utilisateur dans la console après les avoir récupérées via une méthode override ToString() dans la classe User.
  • Pour ceux qui veulent aller plus loin regardez la doc de FastEndpoint et essayez de faire des Endpoints avec Request + Response, c'est pas si dur ;)

Un exemple de code avec l'UI


  • // TODO : Add a sample of use case in .Net with an interface waiting for a async task to be done without being frozen

Programmation parallèle avec Node.js

L'asynchronisme en JS

Nous allons parler d'asynchronisme et faire des exemples en Node.js, mais les principes sont similaires en js, node, typescript,...

Ce n'est pas vraiment le but de la formation d'être complet sur l'asynchronisme en JS, mais nous allons couvrir 3 topics principaux :

 

  • Les callbacks et pourquoi on ne les utilise plus
  • Les Promises, leurs avantages et possibilités
  • L'utilisation d'async / await et comment les utiliser efficacement

J'espère que cette introduction suffira à vous mettre le pied à l'étrier afin d'aller chercher plus d'info par vous même dans les exercices proposés...

Les callbacks

Si vous êtes familier avec javascript, vous connaissez les callbacks, c'est un moyen de passer une fonction en paramètre d'une autre fonction pour qu'elles soient exécutées une fois qu'une opération asynchrone est terminée.

const fs = require('fs');

fs.readFile('file.txt', 'utf-8',
            
  (err, data) => {
    if (err) {
      console.error("Error :", err);
      return;
    }
    console.log("File content :", data);
   }
            
);

Ce troisième paramètre, c'est mon callback

La syntaxe est pas folle de base...

Les callbacks

On peut donc créer autant de callbacks qu'on veut et les enchaîner comme on le souhaite, voyons un exemple plus complet.

Montrer l'exemple CallBackHell

Dans cet exemple, on voit que ça peut vite devenir complexe de maîtriser la pyramide de Doom ou ce qui est nommé callback hell.

C'est pour cette raison que les Promises ont vu le jour et nous allons plutôt nous attarder là dessus !

Et encore... On n'a pas vu comment gérer les erreurs dans cet exemple... Et on ne va pas perdre le temps de le faire.

🤮

Les Promises

Les promises sont le meilleur moyen de mieux gérer ces problèmes d'asynchronisme et de callback hell de js.

Créer une Promise

Appeler une Promise (v1)

const promise = new Promise((resolve) => {
    // Simulate an asynchronous operation
    setTimeout(() => {
        // Resolve the promise
        resolve('Promise resolved')
    }, 2000)
})
promise.then((result) => {
    console.log(result)
})

On créer un objet de type Promise, on lui passe resolve en argument et on peut appeler resolve pour finaliser notre opération promise.

On appelle la promise et avec then((result) =>  {...}) on attend le résultat pour effectuer l'affichage

Ce qui se passe dans le then se passe après le retour de la promise

Ce qui est resolve est passé en paramètre dans le then

Les Promises

On a également la possibilité de gérer les cas d'échecs, avec reject et le catch.

Créer une Promise

Appeler une Promise (v1)

const rejectedPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const random = Math.round(Math.random())
        if (random) 
            resolve('Promise resolved')
        else 
            reject('Promise rejected')
    }, 2000)
})
rejectedPromise.then((result) => {
    console.log(result)
}).catch((error) => {
    console.error(error)    
})

Le random ne sert qu'à pouvoir tester les cas, l'idée est de gérer les cas de réussite avec resolve et les cas d'échecs avec reject.

Cette fois on peut ajouter le catch pour gérer le cas où on est passé dans le reject et qu'on a voulu gérer une erreur

Les Promises

Promise.Any() : Retourne la première promise qui a été résolue, ceci permettant de gérer certains workflow particuliers

Promise.Any()

import fetch from 'node-fetch'
const promisePokemonOne = fetch('https://pokeapi.co/api/v2/pokemon/ditto')
const promisePokemonTwo = fetch('https://pokeapi.co/api/v2/pokemon/pikachu')

const firstResult = Promise.any([promisePokemonOne, promisePokemonTwo])
firstResult
    .then((response) => response.json())
    .then((data) => {
        console.log(`Pokemon type : ${data.types[0].type.name}`)
    })
    .catch((error) => {
        console.error('Error fetching Pokemon data:', error)
    })

Ici, le résultat dépendra de chaque exécution, il sera donc différent d'une exécution à l'autre

Attention ici, fetch retourne une promise et il nous faut le resultat json()

Les Promises

Promise.All() : Attend que toutes les promises passées soient résolues avant de pouvoir chaîner sur les résultats

Promise.All()

const promiseOne = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise one resolved')
    }, 2000)
})

const promiseTwo = new Promise((resolve) => {
    setTimeout(() => {
        resolve('Promise two resolved')
    }, 1000)
})

// This will wait for all promises to be resolved
const allPromises = Promise.all([promiseOne, promiseTwo])

// We can then use the results of all promises that comes as an array
allPromises
    .then((results) => {
        console.log(`1 : ${results[0]}`)
        console.log(`2 : ${results[1]}`)
    })

On va donc pouvoir itérer sur chaque promise les unes après les autres lorsqu'elles seront TOUTES terminées de manière asynchrone.

Les Promises

Promise.All() : Attend que toutes les promises passées soient résolues avant de pouvoir chaîner sur les résultats

Promise.All()

import fetch from 'node-fetch'
const promisePokemonOne = fetch('https://pokeapi.co/api/v2/pokemon/ditto')
const promisePokemonTwo = fetch('https://pokeapi.co/api/v2/pokemon/pikachu')

const allResults = Promise.all([promisePokemonOne, promisePokemonTwo])
allResults
    .then((responses) => Promise.all(responses.map((response) => response.json())))
    .then((data) => {
        console.log(`Pokemon types : ${data[0].types[0].type.name} and ${data[1].types[0].type.name}`)
    })
    .catch((error) => {
        console.error('Error fetching Pokemon data:', error)
    })

Juste un exemple supplémentaire utilisant l'API pokemon, notez la particularité concernant le .map utilisé sur le responses...

Les Promises

Un exemple "concret" de mise en place de Promise et de les chaîner tout en ayant la possibilité de faire autre chose en même temps

Voir PromisesPizza (code)

Pizza

Laundry

Order (2s)

Bake (2s)

Receive (2s)

Eat(2s)

DoLaundry (7s)

Total time

8 secondes

Async / Await

Async & await sont des mots clés importants dans l'asynchronisme en javascript. Ils seront bien souvent préférés à l'utilisation de then / catch pour des plusieurs raisons que l'on listera dans la slide suivante.

Async / await

function getRandomNumber() {
    return new Promise((resolve) => {
        setTimeout(() => {
            const random = Math.floor(Math.random() * 10) + 1
            resolve(random)
        }, 2000)
    })
}

async function main() {
    try {
        const randomNumber = await getRandomNumber()
        console.log(`Random number is ${randomNumber}`)
    } catch (error) {
        console.error(`Error: ${error}`)
    }
}

main()

Dans un contexte de fichier index.js on ne peut pas directement utiliser async / await, c'est pourquoi on créer une méthode "main", on la met en async et on peut utiliser await dedans.

+ Des try / catchs classiques. Et c'est tout !

Async / Await

Quelques remarques sur async / await. Et donc pourquoi choisir plutôt ce moyen en amont des then / catch.

Permet une syntaxe plus propre, du code plus clean
Async / await doivent toujours être utilisés ensemble
Async / await ne concerne que le receveur d'infos
On peut await n'importe quelle promise !
Une méthode async simple retourne une promise en JS
N'importe quelle méthode peut être mise async
Améliore et clarifie la gestion d'erreurs avec les try / catch

Async / Await

Voici exactement le même exemple que précédemment, mais cette fois, c'est fait en utilisant les mots clés async / await

Voir PromisesPizzaAsync (code)

Pizza

Laundry

Order (2s)

Bake (2s)

Receive (2s)

Eat(2s)

DoLaundry (7s)

Total time

8 secondes

Exercices Promises

Premier graphe simple

Il faut donc que

  • A, puis B, puis C soient exécutés et  en parallèle
  • D, puis E puis F.

 

Un simple console log de la lettre suffit.

Voici trois exercices que vous pouvez réaliser pour pratiquer les Promises en le faisant proprement avec l'appels en async / wait.
Vous avez trois graphes à implémenter (voir les 3 slides d'exercices)

Exercices Promises

Voici trois exercices que vous pouvez réaliser pour pratiquer les Promises en le faisant proprement avec l'appels en async / wait.
Vous avez trois graphes à implémenter (voir les 3 slides d'exercices)

Second graphe un peu plus complexe

Il faut donc que

  • A et B soient exécutés en parallèle avant d'exécuter C
  • Après C, on exécute D

Un simple console log de la lettre suffit.

Exercices Promises

Voici trois exercices que vous pouvez réaliser pour pratiquer les Promises en le faisant proprement avec l'appels en async / wait.
Vous avez trois graphes à implémenter (voir les 3 slides d'exercices)

Troisième graphe un peu plus casse tête...

Il faut donc que

  • A s'exécute en premier
  • Puis B, C et D s'exécutent en parallèle
  • Puis E et F s'exécutent en parallèle

Un simple console log de la lettre suffit.

Exercices en typescript

Vous pourrez vous exercer et aller plus loin via ce lien github. Les principes qui y sont présentés sont plus avancés que ce qui a été vu dans le lien précédent. (C'est du typescript ici, mais c'est le lien le plus intéressant et complet que j'ai pu trouver)

Ce lien est vraiment très complet et les exercices sont incroyables pour aller plus loin dans les notions de programmation parallèle avec js. Je vous conseille vivement de faire le maximum d'exercices.

Evaluation

Partie pratique

BON COURAGE !

Evaluation

Réaliser un workflow asynchrone.

Envoyer le fichier Evaluation_PP.md

+ Eval_file1.txt

+ Eval_file2.txt

Calcul somme des nombres de 1 -> 3000

Traiter 2 fichiers calculer le nombre de mots

Traiter 2 fichiers calculer le nombre de "Lorem"

Afficher le résultat de la somme totale des 5 résultats 

Affichage console

Affichage console

Affichage console

N'oubliez pas d'afficher le temps de tout ce traitement et le résultat final !

Utilisez Enumerable.Range(1, 3000)

Partie pratique

BON COURAGE !

Evaluation rattrapage

Réaliser un workflow asynchrone. (Seul le traitement des fichiers s'effectue de manière asynchrone)

Envoyer le fichier Evaluation_PP_rattrapage.md

+ Eval_file1_rattrapage.txt

+ Eval_file2_rattrapage.txt

Calcul somme des nombres de 1 -> 10000

Traiter 2 fichiers calculer le nombre de mots

Traiter 2 fichiers calculer le nombre de "Lorem"

Afficher le résultat de la somme totale des 6 résultats 

Affichage console

Affichage console

Affichage console

N'oubliez pas d'afficher le temps de tout ce traitement et le résultat final !

Faire un appel à l'API et compter le nombre de mot du retour 

Parallèle programming in C#

By Cėdric Brasseur

Parallèle programming in C#

  • 468