Archives Wild Code School
C#

C# - ThreadPool

Quête 36 sur 46

Après cette quête, tu sauras manipuler un ensemble de threads dans un ThreadPool

Objectifs

  • Utiliser une piscine de threads pour faciliter la programmation concurrente

  • Synchroniser des threads dans une piscine avec l'évènement CountdownEvent

  • Choisir des collections concurrentes

Des scores à sortir

Nous avons embauché un stagiaire il y a peu sur notre système légal de génération de scores de courses de chevaux. Il a fait n'importe quoi et nous nous retrouvons maintenant avec un système de génération de scores aléatoires non-fonctionnel.

Tu peux nous aider à nous en sortir ? Tu seras payé par l'acquisition de nouvelles compétences bien entendu ...

Une collection de threads

Tu t'en doutes bien, on tire avantage de la programmation multi-threadée lorsque l'on utilise plusieurs threads en simultané. On peut écrire la gestion de threads simultanée à la main avec une liste simple contenant des objets de type Thread, mais ce serait réinventer la roue !

.NET met à disposition une classe qui permet de gérer l'exécution simultanée d'un ensemble d'objets de type Thread: la classe ThreadPool.

cs
using System.Threading;

public void Run()
{
    for (int i = 0; i < 5; i++)
    {
        ThreadPool.QueueUserWorkItem(x =>
        {
            // Thread delegate
        });
    }

    // Do some work while threads are executing ...
    ThreadPool.Wait(); // Blocks until the end of threads in the pool

    Console.WriteLine("All threads executed");
}

ThreadPool est une classe statique permettant de manipuler un ensemble d'objets de type Thread. Utiliser ThreadPool nous permet d'éviter d'écrire une bonne partie de la gestion d'un ensemble de Thread: synchronisation et lancement de chaque Thread entre autes.

Un ThreadPool est constitué d'une queue. Cette queue contient tous les threads à exécuter. Dès lors qu'un Thread est ajouté à la queue il s'exécute, pas besoin d'appeler la méthode Start(). Pourquoi ne pas appeler la méthode Start() explicitement me diras-tu ? Et bien c'est simplement parce qu'il n'y a pas de création d'objet de type Thread, ils existent déjà dans le ThreadPool à l'état de repos (idle)_. _Ainsi, ThreadPool ne fait qu'invoquer un ou plusieurs de ses Thread lorsqu'il reçoit du boulot dans sa queue. Un ThreadPool est constitué d'une queue. Cette queue contient tous les threads à exécuter. Dès lors qu'un Thread est ajouté à la queue il s'exécute, pas besoin d'appeler la méthode Start(). Pourquoi ne pas appeler la méthode Start() explicitement me diras-tu ? Et bien c'est simplement parce qu'il n'y a pas de création d'objet de type Thread, ils existent déjà dans le ThreadPool à l'état de repos (idle). Ainsi, ThreadPool ne fait qu'invoquer un ou plusieurs de ses Thread lorsqu'il reçoit du boulot dans sa queue.

Et comment est-ce que l'on sait que tous les Thread dans un ThreadPool ont terminé leur traitement ? Il n'y a pas de méthode Join ?

La classe ThreadPool met à disposition une méthode Wait qui bloque l'exécution tant que tous les Thread le constituant ne sont pas terminés.

Et on ne peut pas contrôler plus finement ?

Si, mais il faut l'écrire à la main !

↗
Qu'est-ce qu'un thread pool ?

Article sur ce qu'est un thread pool du point de vue conceptuel

softwareengineering.stackexchange.com
↗
Comment utiliser un ThreadPool ?

Démonstration de l'utilisation d'un ThreadPool pour la gestion de plusieurs threads

owlcation.com

Synchroniser la piscine

ThreadPool met à disposition une méthode Wait, mais elle ne permet pas une gestion fine des Thread exécutés à l'intérieur. En effet, si je veux savoir si un de mes Thread dans le ThreadPool s'est terminé. Il va falloit redoubler de créativité et de logique pour pouvoir synchroniser les traitements exécutés par un ThreadPool.

Une classe vient à notre rescousse: CountdownEvent. L'utilisation de CountdownEvent est simple: cet objet garde un compteur. Le compteur tient référence des Thread en exécution. Ainsi lorsqu'il atteint zéro tous les Thread ont fini de s'exécuter. Simple et élégant, non ?

cs
using System;
using System.Threading;

namespace MultiThreading
{
    public class Program
    {
        private CountdownEvent _countdown;
        private Int32 _threadsCount;

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

        public Program(Int32 threadsCount)
        {
            _threadsCount = threadsCount;
            _countdown = new CountdownEvent(threadsCount); // Set the counter to the number of threads executing
        }

        public void Run()
        {
            for (int i = 0; i < _threadsCount; i++)
            {
                ThreadPool.QueueUserWorkItem(x =>
                {
                    Console.WriteLine("[INFO]: Thread n°{0} has started",
                                      Thread.CurrentThread.ManagedThreadId);

                    Thread.Sleep(500); // Simulate work

                    Console.WriteLine("[INFO]: Thread n°{0} has terminated",
                                      Thread.CurrentThread.ManagedThreadId);
                    _countdown.Signal(); // Decrement the counter of executing threads
                });
            }

            // If there the countdown countdown is greater than zero, there is still work to be done.
            while ( _countdown.CurrentCount > 0)
            {
                Console.WriteLine("[INFO]: Waiting for threads to terminate ...");
                Thread.Sleep(100);
            }

            _countdown.Wait(); // Blocks current thread until the CountdownEvent counter is equal to 0

            Console.WriteLine("[SUCESS]: All threads terminated");
            Console.ReadLine();
        }
    }
}

↗
Comment utiliser CountdownEvent ?

Exemple d'utilisation et explications de CountdownEvent

dotnetpattern.com

Les collections concurrentes

Si tu as joué avec le code précédent, tu as peut-être tenté de partager une collection d'objets entre les threads, et tu as peut-être eu un problème lorsque deux threads différents accèdent à la collection, et cela parce que tu as probablement utilisé une collection de l'espace de noms System.Collections.Generic. Les collections de cet espace de noms ne sont pas thread-safe. C'est-à-dire que si elles sont manipulées par plusieurs threads elles ne fonctionnent pas de manière escomptée.

Un espace de noms met à disposition d'autres collections qui sont thread-safe. Elles sont donc spécialement adaptées aux cas où une même collection est manipulée par des threads différents. Cet espace de noms est System.Collections.Concurrent. Cet espace de noms propose néanmoins des collections moins intuitives au premier abord qu'une collection de List<T>.

En effet, le namespace System.Collections.Concurrent nous donne accès à des piles et des files, qui sont d'autres types de collection qui ne permettent pas d'accéder à leurs éléments par des indices.

La magie des collections concurrentes permet qu'elles soient manipulées par un nombre infini de threads sans planter grossièrement et de manière non-prévisible.

↗
Quelles sont les collections disponibles ?

Référence officielle sur l'espace de noms System.Collections.Generic

docs.microsoft.com
↗
Que sont les piles et les files ?

Les piles et les files du point de vue scientifique de l'informatique

everythingcomputerscience.com
↗
Comment utiliser les collections concurrentes ?

Explications par l'exemple sur les collections concurrentes

devsanon.com

Challenge

Un générateur de scores

Tu vas écrire un programme qui génère un nombre très important de scores en programmant de manière concurrentielle.

Critères de validation

  • La solution est hébergée sur GitHub

  • Une liste concurrente d'entiers est peuplée par au moins trois Thread dans un ThreadPool

  • Le programme se termine lorsque la liste est constituée d'au moins 1 millions de scores