Programmation multithread
Avec l’arrivée de l’hyperthreading des Pentium 4, et les futurs multicoeur chez AMD et Intel, la programmation multithread risque de devenir incontournable. En tout cas un développeur sérieux se doit de savoir de quoi il s’agit
Quelques notions
Un thread est un chemin d’exécution à l’intérieur d’un programme. C’est en général la plus petite unité d’exécution séquencé par le système d’exploitation.
Le contexte d’un thread consiste souvent en une pile d’exécution (stack), un l’état des registres du CPU et une entrée dans dans le séquenceur du système d’exploitation.
Chaque thread partage les ressources du process qui l’héberge avec les autres threads de ce process.
Un process consiste donc en un ou plusieurs threads, ainsi que le code, les donnée et les ressources en mémoire.
On comprends mieux l’intérêt du multithread en prenant une analogie. Si un ordinateur était une usine et un process un batiment dédié à une fabrication particulière, chaque thread correspond à une ligne de fabrication. Chaque processeur logique étant comme un ouvrier, il peut passer d’une ligne à une autre mais cela prend du temps : c’est le changement de contexte qui a lieu lorsque le système d’exploitation bascule d’un thread à l’autre. Par contre en augmentant le nombre d’ouvrier (de processeurs logique) on augmente le nombre de ligne de fabrication (de thread) exploitable en parallèle. Quand je parle de processeur logique, il s’agit d’une entité capable d’exécuter un thread. Dans le cas d’un processeur bi-coeur supportant l’hyperthreading, on a 4 processeurs logiques.
Intérêt du multithread
Le multithread est supporté par tous les OS modernes (Windows XP, Linux, Mac OS X). Il n’est pas nécessaire d’avoir une configuration multi-processeur ou multi-coeur (réel ou virtuel avec l’hyperthreading) pour exécuter plusieurs threads. Par contre il ne s’exécute réellement en parallèle qu’un nombre de thread correspondant au nombre de processeurs virtuels. Le changement de contexte rapide donne l’illusion d’une exécution en parallèle d’une nombre élevé de thread, mais au delà d’une certaine quantité de thread, les performances s’effondre (évidement le seuil dépend de la configuration matérielle et de l’OS).
Il faut bien comprendre que l’hyperthreading et les processeurs multi-coeur sont développé par Intel et AMD parce que la course à la fréquence s’essouffle. Les dernières générations de gravure n’ont pas parmi d’obtenir les fréquences espérés, et le passage à l’hyperthreading et au multi-coeur est un moyen d’augmenter la puissance des processeurs sans augmenter leurs fréquences.
A condition d’avoir des applications exploitant le multithread. En effet si un seul thread s’exécute sur une machine multi-processeur ou multi-coeur, on n’observe pas d’amélioration des performances par rapport à une machine mono-processeur et mono-coeur (à fréquence et architecture égale, bien sur). Les anciennes applications de type bureautique sont souvent monothread et ne profiteront pas des architecture multi-coeur. Par contre les applications serveur sont en général par nature multithread, chaque requête clients s’exécutant dans un thread indépendant.
Les gains que l’on peut obtenir ne sont pas toujours à la hauteur des espérances. Au mieux la performance sera proportionnelle au nombre de processeurs, mais en fait on obtient entre 30 et 50 % de gain par unités de traitement logique supplémentaire.
Difficultés du multithread
Tous les threads d’un process partage ses ressources : fichiers, variables statiques, mémoire tas (heap). Il faut gérer l’accès exclusif d’un thread à ces ressources partagés.
Chaque thread possède une pile de mémoire. Il faut déterminer au plus juste cette taille. Si elle est trop petite et un thread risque ne pas pouvoir allouer les variables dont il a besoin et trop grand elle augmente le coût du changement de contexte et le nombre de thread que le système peut gérer.
Les objets de synchronisation
Il existe de multiples objets de synchronisation fournit par les système d’exploitation pour protéger l’accès aux ressources partagés. Selon les cas on utilisera, dans le cas de Windows :
- Evenement
- Si le thread doit attendre que quelque chose arrive avant d’accéder à une ressource.
- Sémaphore
- Si plusieurs thread de la même application peuvent accéder à une ressource.
- Mutex
- Pour limiter l’accès à une ressource commune à plusieurs applications (fichier) à un seul thread à la fois.
- Section critique
- Pour limiter l’accès à une ressource spécifique à une applications (objets, zone mémoire) à un seul thread à la fois.
- Autres objets
- Sous windows il existe aussi les méthodes “interlocked access” permettant l’incrementation, la décrementation ou l’échange protégé de variable entière de 32 bits d’un process.
Les I/O completion port offre une manière native de gérer des traitements asynchrone par une réserve de threads de traitement.
Programmation objet et multithreading
Bien sur la plupart des langages permettent la programmation multithread. Cependant les langages orientés objets permettent de contrôler plus facilement le fait qu’un code est thread-safe. C’est à dire capable de s’exécuter sans risque dans un contexte multithread, soit parce qu’il n’y a pas d’accès à des ressources partagés, soit que cet accès est protégé par d’utilisation d’objets de synchronisation.
Il convient donc de :
- Définir les méthodes ne modifiant pas un objet comme constantes. En effet les méthodes constantes sont par définition thread-safe.
- Encapsuler les attributs des objets par des méthodes d’accès constantes. Cela permet d’accéder aux attributs sans risque.
- Protéger toutes les méthode non constantes par des objets de synchronisation. Certains langage (C#, Java) propose des constructions natives (
lock). Selon le nombre d’objet d’une classe, on utilisera un objet de synchronisation interne à chaque objet d’une classe peu nombreuse ou partagés dans le cas de nombreux objets.
Et moi, je fais quoi ?
La plupart des environnement de développement web utilise le multithread mais d’une manière en général transparente pour le développeur. Par exemples en ASP.Net chaque requête est traité dans un thread, donc le code de traitement d’une page est exécuté dans le même thread. Si on n’utilise pas de variable statique on ne voit pas la différence. Mais dans le cas contraire il faut que l’accès au ressources dans cette variable statique soit protégé.
Dans le cas d’une application Windows, il peut être intéressant d’avoir un ou plusieurs thread gérant l’interface, et un ou plusieurs thread exécutant les traitements. Par contre la synchronisation peut devenir complexe.
Le vrai problème du multithread c’est le surcoût en matière de tests. En effet l’obtention de tests unitaires complets est loin d’être facile en multithread. Sans compter que certains problème de synchronisation n’apparaissent que dans des environnements multiprocesseurs, puisque c’est le seul cas ou plusieurs threads sont réellement exécuté en parallèle.
Donc le choix de développer une application multithread devra se faire en tenant compte des avantages (gain de performances avec les processeurs récents et futurs) et des inconvenients (plus grande difficulté de mise au point). Par contre vu que le passage en mode multithread d’une application non conçu pour cela peut-être très couteux, il vaut mieux y penser avant !
avril 6th, 2005 à 0:07
Très belle synthèse sur le sujet et ses différentes facettes.
avril 6th, 2005 à 0:17
Merci. Certains de mes anciens collègues reconnaîtront vaguement le plan d’une formation que j’avais fait il y a quelques années.
juillet 9th, 2008 à 7:35
merci pour vous sur cette expliquation mais il ya un manque des exemples .svp