4 Les modèles de programmation asynchrone
4.3 Premier outil des APM : La classe ThreadPool
Jusqu’à présent nous avons crée nos propres threads asynchrone « à la main ». Si cette manière de faire paraît intéressante lorsque nous n'utilisons qu’un seul thread asynchrone, cela devient fastidieux en cas de multithreading.
Le Framework .NET met à notre disposition un pool de threads afin de gérer le multithreading de manière simple et optimisée. Un pool de thread contient un nombre de thread minimum et maximum que vous pouvez utiliser en continu. En effet les threads libérés sont immédiatement réutilisable et ce dans la limite de disponibilité des threads. Si aucun thread n’est disponible, les opérations suivante nécessitant un thread sont mise en file jusqu’à ce qu’un thread du pool soit disponible.
Cette façon de faire permet non seulement de gérer le multithreading facilement, mais également plus rapidement.
4.3.1 Utilisation standard du pooling de threads
Pour travailler avec le pool de threads vous devez utiliser les membres statiques de la classe ThreadPool :
Méthodes Statiques Description
GetAvailableThreads Retourne le nombre de thread disponibles dans le pool
GetMaxThreads Retourne le nombre maximum de thread que supporte ce pool
GetMinThreads Retourne le nombre minimum de thread que supporte ce pool.
QueueUserWorkItem Met en queue une opération, si un thread est disponible, elle est immédiatement exécuté, sinon elle attend.
RegisterWaitForSingleObject Permet d’inscrire un délégué pour attendre un WaitHandle. (Voir Partie 2).
Prend également un délai en paramètre pour faire expirer automatiquement le thread.
SetMaxThreads Permet de définir le nombre maximum de threads du pool. Sachez que le nombre de threads dans le pool doit toujours être plus grand que le nombre de processeur de votre machine ou de cœurs dans votre processeur.
SetMinThreads Permet de définir le nombre minimum de threads du pool. Sachez que le nombre de threads dans le pool doit toujours être plus grand que le nombre de processeur de votre machine ou de cœurs dans votre processeur.
UnsafeQueueNativeOverlapped Permet de mettre en queue une opération
d’entrée/sortie avec chevauchement. Voir le chapitre 13 sur l’Interopération.
Cette méthode peut ouvrir des failles de sécurité ! UnsafeQueueUserWorkItem Met en queue un objet sans les informations de
contexte et la pile appelante, cette méthode peut ouvrir des failles de sécurité !
UnsafeRegisterWaitForSingleObject Permet de définir un délégué qui attend un évènement WaitHandle. Ne propage pas les
informations de contexte et la pile appelante. Cette
Redéfinir les bornes du pool de Thread permet d’adapter le pool à votre application.
Elever le nombre de Thread maximum pouvant travailler en parallèle va vous permettre d’effectuer beaucoup plus de traitements simultanément, et donc d’accélérer les opérations. Cela est vrai seulement si les tâches ne sont pas trop lourdes, auquel cas, faire tourner trop de threads lourd en simultané aura un impact sur les performances.
Elever le nombre de Thread Minimum va vous permettre d’obtenir dès lancement de l’application des threads déjà prêts à travailler. Ainsi, vous pourrez effectuer plus rapidement vos opérations. Cela est particulièrement utile si vous avez de nombreuses opérations à effectuer au lancement de votre application. En effet, créer de nouveaux threads dans le pool, après son démarrage, prendra beaucoup de ressources et pourra nuire aux performances.
Nous allons illustrer l’utilisation du pool de threads avec un exemple. Nous allons faire faire une boucle simple à plusieurs threads du pool, et nous limitons à deux Threads maximum le pool.
'VB
Public Sub affichageThreads(ByVal o As Object) Dim chaine As String = CType(o, String) Dim i As Integer = 0
While i < 2
Console.WriteLine("{0} {1}", chaine, Thread.CurrentThread.ManagedThreadId)
i += 1
Thread.Sleep(100) End While
End Sub Sub Main()
Dim threads As Integer
Dim completionPort As Integer ThreadPool.SetMaxThreads(2, 100) ThreadPool.SetMinThreads(2, 100)
ThreadPool.GetAvailableThreads(threads, completionPort) Console.WriteLine("Nombre de Threads disponibles : {0}", threads.ToString())
Dim objet As WaitCallback = New WaitCallback(AddressOf affichageThreads)
For i As Integer = 0 To 10 Step 1
If (Not ThreadPool.QueueUserWorkItem(objet, "Execution d'une opération du thread")) Then
Console.WriteLine("Impossible de rajoute cet objet au Pool")
End If Next
Console.Read() End Sub
Nombre de Threads disponibles : 2
//C#
static void affichageThreads(object o) {
string chaine = o as string;
for (int x = 0; x < 2; ++x) {
Console.WriteLine("{0}
{1}",chaine,Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
} }
static void Main(string[] args) {
int threads;
int completionPort;
ThreadPool.SetMaxThreads(2, 100);
ThreadPool.SetMinThreads(2, 100);
ThreadPool.GetAvailableThreads(out threads, out completionPort);
Console.WriteLine("Nombre de Threads disponibles : {0}", threads.ToString());
WaitCallback objet = new WaitCallback(affichageThreads);
for (int i = 0; i < 10; i++) {
if (!ThreadPool.QueueUserWorkItem(objet, "Execution d'une opération du thread"))
{
Console.WriteLine("Impossible de rajoute cet objet au Pool");
} }
Console.Read();
}
Execution d'une opération du thread 6 Execution d'une opération du thread 6 Execution d'une opération du thread 6 Execution d'une opération du thread 6 […]
Execution d'une opération du thread 10 Execution d'une opération du thread 6 Execution d'une opération du thread 10 Execution d'une opération du thread 6 Execution d'une opération du thread 6
Nous avons donc une méthode effectuant une boucle simple affichant le numéro d’identification du thread. La méthode appelée doit forcement avoir un objet en paramètre même s’il n’est pas utilisé.
Dans notre Main nous commençons par définir les minimums et maximums de notre ThreadPool et comme vous pouvez le voir, la méthode prend deux arguments. Le second argument correspondant aux limites des complétions ports qui sont des threads réservés aux entrées/sorties asynchrone.
Ensuite nous affichons le nombre de threads disponibles avant de commencer. Dans notre cas, le résultat est deux, qui est le minimum sur la machine d’exemple. Remarquez que pour récupérer le nombre de threads, nous avons passé une variable par référence à notre méthode.
Ensuite nous créons notre délégué qui assignera une méthode à chaque thread jusqu’à la fin de la boucle for. Nous utilisons la méthode QueueUserWorkItem pour assigner nos opérations à un thread et nous vérifions que tous les objets ont pu être mit en file au cas où arriverait un problème.
4.3.2 ThreadPool et WaitHandle
Nous l’avons vu dans la partie 2 que les objets Mutex, Semaphore et Event héritent de la class WaitHandle. Le pool de Thread supporte également le verrouillage fourni par WaitHandle. Nous pouvons ainsi faire en sorte que certains threads attendent un signal avant de d’appeler des méthodes. Afin d’utiliser WaitHandle, nous allons nous servir de la méthode RegisterWaitForSingleObject qui va prendre en paramètre notre objet de verrouillage (par exemple un mutex), un délégué de type WaitOrTimerCallback qui se déclenche au signal ou à l’expiration du compteur, un objet qui va accompagner le callback, le temps avant expiration et enfin un booléen indiquant si on veut réinitialiser ou non le compteur une fois le temps expiré.
Le Mutex a reçu le signal 'VB
Public Sub mutexDeclenche(ByVal etat As Object, ByVal tempsEcoule As Boolean)
If (tempsEcoule) Then
Console.WriteLine("Le Mutex est expiré") Else
Console.WriteLine("Le Mutex a reçu le signal") End If
End Sub Sub Main()
Dim mutex As Mutex = New Mutex(True)
ThreadPool.RegisterWaitForSingleObject(mutex, New
WaitOrTimerCallback(AddressOf mutexDeclenche), Nothing, 1000, True) mutex.ReleaseMutex()
Console.Read() End Sub
//C#
static void mutexDeclenche (object etat, bool tempsEcoule) {
if (tempsEcoule)
Console.WriteLine("Le Mutex est expiré");
else
Console.WriteLine("Le Mutex a reçu le signal");
}
static void Main(string[] args) {
Mutex mutex = new Mutex(true);
ThreadPool.RegisterWaitForSingleObject(mutex, new WaitOrTimerCallback(mutexDeclenche), null,1000 , true);
mutex.ReleaseMutex();
Console.Read();
}
Nous avons donc instancié notre Mutex que nous enregistrons dans le pool de threads avec une durée limite de 1 seconde. Si le verrouillage fourni par le Mutex n'est pas débloqué avant cette seconde d'attente, la méthode callback passée en paramètre verra sa propriété "tempsEcoule" être définie a false.
Ensuite nous utilisons la méthode ReleaseMutex pour lever le verrouillage et ainsi déclencher l’événement. La méthode est donc lancée et nous indique que le signal a été reçu dans les temps.
Notre méthode mutexDeclenche prend obligatoirement deux paramètres minimum, l’état du thread et un booléen indiquant si le temps avant expiration est écoulé.