Archives Wild Code School
C#

C# - Multi-threading

Quête 33 sur 46

Après cette quête, tu sauras créer et utiliser des threads qui réalisent des traitements de manière asynchrones

Un course de chevaux

Oh mince, le programme de course de chevaux automatique ne marche plus ! Eh oui, tu ne le savais pas ? Nous manipulons 97% de nos course de chevaux avec des programmes qui donnent des résultats aléatoires. Le stagiaire ayant écrit notre programme est parti en laissant une solution non-fonctionnelle. Aurais-tu la patience de nous en écrire un nouveau ?

Programmation concurrente

Parmis tous les paradigmes de programmation existants, la paradigme concurrent est un des plus prompts à buguer et des plus difficiles à gérer.

Qu'est-ce qui rend si difficile la programmation concurrente ?

Et bien, la programmation concurrente consiste à exécuter plusieurs tâches en simultané sur un ordinateur. Il est ainsi possible de réaliser des calculs et des traitements parallèlement les uns aux autres, dans l'espoir d'un gain de performance. Le multi-threading est très utilisé dans le monde du jeu vidéo: comment rouler en voiture et ajuster la lumière en fonction du placement des ombres et du soleil sur le pare-brise de la voiture roulée en maximisant les performances ? En utilisant la programmation concurrente bien sûr ...

↗
Qu'est-ce que la programmation concurrente ?

Article sur la programmation concurrente

educative.io
↗
Le parallélisme en C#

Tutoriel sur le parallélisme

vincentlaine.developpez.com

Créer et exécuter un thread

Avant d'entrer dans le vif du sujet, j'aimerais introduire un nouveau type d'objet fondamental dans la programmation parallèle: les threads.

Un thread est une unité d'exécution indépendante. C'est-à-dire qu'elle s'exécute indépendamment des autres programmes.

Bah, comme un programme quoi !

Et bien non, un thread n'a pas les mêmes attributs qu'un process (programme). En effet, un thread est attaché à un process et partage son espace mémoire, tandis qu'un process possède son propre espace mémoire. Ainsi, un process peut lancer plusieurs threads qui lui sont attachés.

Bien que le modèle concurrent soit séduisant il n'est pas une réponse absolue aux problèmes de performance et doit être choisi en connaissant ses risques. Et oui, car un développement multi-threadé est en somme assez risqué pour plusieurs raisons:

  • Il faut synchroniser l'exécution des threads entre eux

  • Il faut synchroniser l'accès aux données

Assez parlé, voici comment on crée un Thread:

cs
using System;
using System.Threading;

namespace MultiThreading
{
    class Program
    {
        public static void Main()
        {
            var threadStartDelegate = new ThreadStart(OnThreadStart);
            var newThread = new Thread(threadStartDelegate);
        }

        private void OnThreadStart()
        {
            System.Console.WriteLine("Hello, I am the thread n°", Thread.CurrentThread.ManagedThreadId);
        }
    }
}

Ce code mérite d'être rapidement décortiqué:

  • On associe un delegate exécuté au lancement du Thread dans un objet ThreadStart

  • On créer un Thread à partir d'un objet ThreadStart

  • Le delegate OnThreadStart affiche un message contenant l'id numérique du Thread en exécution

↗
Comment créer un thread ?

Court tutoriel sur la création d'un thread

csharp-examples.net
↗
Comment utiliser les thread en C# ?

Tutoriel complet sur le namespace Threading

docs.microsoft.com
↗
Qu'est-ce qu'un code Thread-safe ?

Fil de discussion sur le code Thread-safe

stackoverflow.com

Joindre les threads

Pour s'assurer que des threads soient terminés avant de continuer l'exécution du code dans le process principal, on joint les threads. Et pour ce faire, rien de plus simple:

csharp
using System.Threading;

class Program
{
    public static void Main()
    {
        var threadStartDelegate = new ThreadStart(OnThreadStart);
        var threads = new List<Thread> { 
            new Thread(threadStartDelegate),
            new Thread(threadStartDelegate)
        };
        foreach(var t in threads)
        {
            t.Start(); // We launch the threads
        }

        // Do something while threads are executing ...

        foreach(var t in threads)
        {
            t.Join(); // We wait until all threads are finished
        }
    }

    private void OnThreadStart()
    {
        var random = new System.Random();
        var executionTime = random.Next(2, 7);
        var timeSpan = TimeSpan.FromSeconds(executionTime);
        Thread.Sleep(timeSpan); // Simulate computing by waiting a random period of time
    }
}

Dans le code ci-dessus, on crée 2 threads que l'on lance puis joint à la fin. Ainsi, le programme se termine lorsque les deux threads crée ont terminé leurs traitements simultanés.

↗
Pourquoi joindre des threads ?

Fil de discussion sur la jointure de thread

stackoverflow.com
↗
Thread.Join

Référence officielle de la méthode Thread.Join

docs.microsoft.com

Protéger les données

Un autre point non-négligeable du multi-threading est le partage de données. Il est difficile de mettre en place une architecture logicielle permettant un partage simple des données entre plusieurs threads, de même qu'il existe de multiples manières d'y arriver. Je vais te montrer la plus simple:

csharp
using System.Threading;

class Program
{
    private Int32 SharedInteger { get; set; }

    public static void Main()
    {
        var program = new Program();
        program.Run();
    }

    public void Run()
    {
        var threadStartDelegate = new ThreadStart(OnThreadStart);
        var threads = new List<Thread> { 
            new Thread(threadStartDelegate),
            new Thread(threadStartDelegate)
        };
        foreach(var t in threads)
        {
            t.Start(); // We launch the thread
        }

        // Do something while threads are executing ...

        foreach(var t in threads)
        {
            t.Join(); // We wait until all threads are finished
        }

    private void OnThreadStart()
    {
        var random = new System.Random();
        var executionTime = random.Next(2, 7);
        var timeSpan = TimeSpan.FromSeconds(executionTime);
        SharedInteger += timeSpan.Milliseconds;
        Thread.Sleep(timeSpan); // Simulate computing by waiting a random period of time
    }
}

Dans l'exemple au-dessus (qui ressemble en tous points à l'exemple de l'étape précédente), une donnée est partagée entre plusieurs threads et il s'agit de l'entier SharedInteger. Cet entier est incrémenté du nombre de millisecondes généré aléatoirement à chaque exécution d'un thread.

Tout semble bien se passer jusque-là, mais que se passe t-il si plusieurs threads accèdent à la même donnée simultanément ?

Ah ça ... C'est la bête noire de tous les développements multi-threadés: gérer l'accès asynchrone à des données partagées entre plusieurs threads. Il existe plusieurs méthodes pour synchroniser les threads. Celle que je vais te montrer est l'utilisation de sémaphores (ou mutex).

Un mutex ?

Oui, un mutex. C'est une entité logicielle qui permet de _verouiller _des zones de code et des ressources jusqu'à nouvel ordre. Ainsi, si nous voulons un code thread-safe, voici ce que nous devons rajouter dans le delegate de nos threads:

csharp
using System.Threading;

class Program
{
    private Int32 SharedInteger { get; set; }
    private Mutex mutex = new Mutex();

    // ...

    private void OnThreadStart()
    {
        var random = new System.Random();
        var executionTime = random.Next(2, 7);
        var timeSpan = TimeSpan.FromSeconds(executionTime);
        
        mutex.WaitOne(); // Wait for the mutex to be released
        SharedInteger += timeSpan.Milliseconds;
        Thread.Sleep(timeSpan); // Simulate computing by waiting a random period of time
        mutex.ReleaseMutex(); // Release when computing is done
    }
}

Le principe des mutex est simple, c'est un verrou. De la manière la plus simple possible, on verrouille l'accès à une partie du code en demandant l'acquisition du mutex (Mutex.WaitOne). Une fois le mutex acquis, aucune autre portion de code attendant l'acquisition du même mutex ne pourra s'exécuter. Il(s) devront attendre que le mutex soit libéré par le thread l'ayant acquis. Cette technique permet de protéger des ressources partagées. Rien de plus simple !

↗
Comment synchroniser les threads entre eux avec un mutex ?

Référence officielle de la classe Mutex

docs.microsoft.com
↗
Comment synchroniser les threads entre eux sans mutex ?

Tutoriel sur la synchronisation des threads en utilisant des locks

javatpoint.com

Challenge

1, 2, 3 partez !

Tu vas écrire un programme permettant de déterminer quel thread est le gagnant dans une course.

Critères de validation

  • La solution est hébergée sur GitHub

  • La course des threads est simulée par une attente sur une période aléatoire lors de leur exécution

  • Une fois que tous les threads sont terminés, le programme affiche le thread gagnant (son id)

  • Un mutex est appliqué lors de la modification du valeur permettant de retrouver le thread gagnan