C# - ThreadPool
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.
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
Threaddans unThreadPoolont terminé leur traitement ? Il n'y a pas de méthodeJoin?
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 !
Article sur ce qu'est un thread pool du point de vue conceptuel
Démonstration de l'utilisation d'un ThreadPool pour la gestion de plusieurs threads
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 ?
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();
}
}
}
Exemple d'utilisation et explications de CountdownEvent
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.
Référence officielle sur l'espace de noms System.Collections.Generic
Les piles et les files du point de vue scientifique de l'informatique
Explications par l'exemple sur les collections concurrentes
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
Threaddans unThreadPool -
Le programme se termine lorsque la liste est constituée d'au moins 1 millions de scores