Multi-threading avec .NET : Introduction

Le multi-threading est probablement l'un des aspects le moins bien compris de la programmation, et aujourd'hui presque tous les développeurs d'applications ont besoin de le connaître, au moins dans une certaine mesure. Cet article est une introduction à la programmation multi-thread et apporte quelques astuces et recommandations pour la pratiquer de façon correcte. Attention : je ne suis pas un expert sur le sujet, cependant, j'ai observé ceux qui savent ce qu'ils font, et j'ai essayé de faire de cet article une sorte de "best practice" de la programmation multi thread.

Cet article utilise les abréviations des types C# (int pour Int32...). J'espère que cela sera facilitera la lisibilité pour les développeurs C# et ne gênera pas trop les autres développeurs. De même, les syntaxes utilisées pour déclarer une variable volatile ou verrouiller un moniteur sont propres au C#. Les développeurs utilisant d'autres langages peuvent trouver leurs équivalents dans leur propre environnement.

Introduction : Qu'est que la programmation multi-thread?

Le fait que vous lisiez cet article indique que vous avez probablement déjà une idée sur le sujet. Basiquement, programmation multi-thread, c'est essayer d'exécuter plusieurs tâches simultanément dans un même processus.

Mais alors qu'est-ce qu'un thread? Un thread est une sorte de contexte dans lequel s'exécute le code. Et tout thread suit le flot des instructions pour lequel il a été créé. En réalité, avant de faire du multi-threading, il y a toujours au moins un thread qui tourne pour chaque processus du système d'exploitation. Une bonne façon de visualiser le threading est de penser à des processus tournant en parallèle dans un système d'exploitation (par exemple un navigateur téléchargeant un fichier et une application de traitement de texte tournant tous les deux en même temps), le threading revient à appliquer le même mode de pensé mais à l'intérieur d'un seul processus.

Le multi-threading peut se faire de façon "réelle" dans une machine multi-processeurs, si plus d'un processeur exécutent simultanément des instructions pour un même processus. Mais il peut aussi se faire de façon "simulée" quand plusieurs threads sont exécutés en séquence : exécution d'un peu de code du thread 1, ensuite, un peu de code du thread 2 est exécuté puis retour au thread 1... Dans cette situation, si les deux threads 1 et 2 sont à pleine charge (ils ne font que du calcul sans attendre aucune entrée du réseau, du système de fichier ou de l'utilisateur) le mutli-threading ne va pas accélérer les choses, en fait il va même les ralentir puisque le système d'exploitation doit passer d'un thread à l'autre et le cache mémoire perdra donc en efficacité. Mais aujourd'hui la plupart des programmes informatiques impliquent des attentes (pour l'arrivée d'un événement) durant lesquelles le processeur peut faire d'autres choses. La technologie "Hyper-Threading" d'Intel qui est intégrée dans certains processeurs récents (gardez à l'esprit que cet article a été écrit au début de l'année 2004) est une sorte d'hybride entre le threading "réel" et le threading "simulé". Pour plus d'information référez vous à la page d'Intel sur le sujet.

Comment marche le multi-threading avec .NET?

.NET a été conçu depuis le début pour supporter des opérations multi-threadées et propose 2 principales techniques de multi-threading :

Le thread pool ne peut faire tourner qu'un nombre limité de tâche en même temps, et certaines classes du framework l'utilise en interne. Il est donc préférable, pour les longues tâches de créer un nouveau thread "manuellement". D'un autre côté, pour les courtes tâches et particulièrement pour celles qui sont exécutées plusieurs fois, le thread pool est un excellent choix. Les exemples de cet article utilisent principalement la création "manuelle" de thread.

Mon premier programme multi-thread

Voici un exemple utilisant des threads et pour lequel il y a un résultat visible :

using System;
using System.Threading;

public class Test
{
    static void Main()
    {
        ThreadStart job = new ThreadStart(ThreadJob);
        Thread thread = new Thread(job);
        thread.Start();
        
        for (int i=0; i < 5; i++)
        {
            Console.WriteLine ("Main thread: {0}", i);
            Thread.Sleep(1000);
        }
    }
    
    static void ThreadJob()
    {
        for (int i=0; i < 10; i++)
        {
            Console.WriteLine ("Other thread: {0}", i);
            Thread.Sleep(500);
        }
    }
}

Ce code créé un nouveau thread à partir de la méthode ThreadJob et le démarre. Ce thread compte de 0 à 9 assez rapidement (2 incréments par seconde) pendant que le thread principal (Main thread) compte de 0 à 4 plus lentement (1 incrément par seconde). Pour les faire compter à différente vitesse, on utilise la méthode Thread.Sleep qui permet de faire dormir (ne rien faire et laisser le processeur libre pour les autres tâches) le thread courant pendant une période de temps spécifiée. Entre chaque incrémentation, le thread principal dort pendant 1000ms alors que l'autre thread dort pendant 500ms. Voici les résultats d'un test sur ma machine :

Main thread: 0
Other thread: 0
Other thread: 1
Main thread: 1
Other thread: 2
Other thread: 3
Main thread: 2
Other thread: 4
Other thread: 5
Main thread: 3
Other thread: 6
Other thread: 7
Main thread: 4
Other thread: 8
Other thread: 9

L'un des points les plus important ici est que, bien que le résultat ci-dessus soit très régulier, cela n'est dû qu'au hasard. Il n'y a rien qui empêche la première ligne "Other thread" d'arriver avant la première ligne "Main thread" ou le motif d'être légèrement différent. Thread.Sleep est toujours approximative et il n'y a aucune garantie pour que le thread endormit recommence à s'exécuter immédiatement après la fin du Sleep. (Il se peut qu'il s'exécute, mais un autre thread peut déjà être en train de tourner, et sur une machine mono-processeur cela signifie que le thread qui vient d'être réveillé devra attendre que le scheduler décide de lui donner la main).

Comme pour tous les delegates, il n'y a rien qui vous interdise d'utiliser des méthodes de classe (static) ou des méthodes d'instance pour la création d'un delegate. Bien sûr vous avez besoin d'avoir accès à la méthode, et si vous voulez utiliser une méthode d'instance, vous devez spécifier une instance particulière. Voici une nouvelle version du programme utilisant une méthode d'instance d'une autre classe (différente de celle qui fait tourner le thread). Si la méthode Count avait été statique, la valeur de la variable job aurait été new ThreadStart(Counter.Count). La plus part des exemples de cet article utilisent des méthodes provenant de la même classe, mais cela est uniquement pour simplifier le code.

using System;
using System.Threading;

public class Test
{
    static void Main()
    {
        Counter foo = new Counter();
        ThreadStart job = new ThreadStart(foo.Count);
        Thread thread = new Thread(job);
        thread.Start();
        
        for (int i=0; i < 5; i++)
        {
            Console.WriteLine ("Main thread: {0}", i);
            Thread.Sleep(1000);
        }
    }
}

public class Counter
{
    public void Count()
    {
        for (int i=0; i < 10; i++)
        {
            Console.WriteLine ("Other thread: {0}", i);
            Thread.Sleep(500);
        }
    }
}

Page suivante : Passer des paramètres aux threads


Lire la version originale de cette page

Revenir sur la page d'accueil de sylvain114