Principe

Les threads permettent d’avoir plusieurs fils d’exĂ©cution au sein d’un processus qui s’exĂ©cutent en mĂȘme temps.

Les diffĂ©rents threads d’un processus partagent les mĂȘmes donnĂ©es.

Les dangers de partager les mĂȘmes donnĂ©es

Si plusieurs threads utilisent les mĂȘmes donnĂ©es, il faut alors mettre en place des protections (mĂ©canismes d’exĂ©cution mutuelle par exemple)

Pour plus d’informations, voir Partage de donnĂ©es et la data-concurrence

IntĂ©rĂȘts des threads

Il y a de nombreux intĂ©rĂȘts :

  • Performance (si on a plusieurs processeurs) : Nous pouvons dans ce cas partager les tĂąches sur les diffĂ©rents cƓurs du processeur.
  • Organisation du programme
  • Effectuer diffĂ©rentes opĂ©rations en simultanĂ©es
  • etc.

Termes et vocabulaires

Concepts fondamentaux :

  • Thread : Un fil d’exĂ©cution au sein d’un processus. Imaginez plusieurs tĂąches s’exĂ©cutant en parallĂšle dans un mĂȘme programme.
  • Processus : Un programme en cours d’exĂ©cution. Un processus peut contenir plusieurs threads.
  • Concurrence : La capacitĂ© d’exĂ©cuter plusieurs tĂąches en mĂȘme temps.
  • ParallĂ©lisme : L’exĂ©cution simultanĂ©e de plusieurs tĂąches sur plusieurs cƓurs de processeur (comme mentionnĂ© dans Principe).
  • Deadlock : Une situation oĂč deux threads ou plus sont bloquĂ©s indĂ©finiment, chacun attendant qu’un autre libĂšre une ressource. (voir Deadlock)
  • Race condition : Une situation oĂč le rĂ©sultat d’une opĂ©ration dĂ©pend de l’ordre d’exĂ©cution des instructions de plusieurs threads. (voir Dataracing)
  • Threads lĂ©gers : Des threads qui ont une empreinte mĂ©moire plus faible et peuvent ĂȘtre créés en grand nombre. (voir ‘Green’ threads)
  • MĂ©moire partagĂ©e : Une zone de mĂ©moire accessible par tous les threads d’un processus.
  • Planificateur : Le composant du systĂšme d’exploitation qui dĂ©cide quel thread doit s’exĂ©cuter Ă  un moment donnĂ©. (voir Threads systĂšmes)

Mise en Ɠuvre (C)

En C, la bibliothĂšque pthread (POSIX threads) peut ĂȘtre utilisĂ©e

Créer et démarrer un nouveau thread

#include <pthread.h>
 
int pthread_create(pthread_t *thread,
				   pthread_attr_t *attr,
				   void *(*start_routine)(void*),
				   void *arg)

Arguments de la fonction :

  • start_routine: (function (void v) -> void*)
  • thread: L’identifiant du thread créé est enregistrĂ© dans *thread.
  • attr: permet de passer des options pour la crĂ©ation du thread (NULL pour les valeurs par dĂ©faut)

Attendre un thread

On peut attendre la fin d’exĂ©cution du thread en utilisant la fonction :

int pthread_join(pthread_t thread, void** retval)

OĂč thread est l’identifiant du thread Ă  attendre, et retval permet de rĂ©cupĂ©rer sa valeur de retour.

Terminer un thread

Le thread se termine à la fin de sa fonction, ou bien en utilisant la fonction :

void pthread_exit(void* retval)

OĂč retval est la valeur retournĂ©e par le thread.

Exemple

#include <stdio.h>
#include <pthread.h>
 
// Fonction exécutée par le nouveau thread
void* task(void* arg) {
    int thread_id = *((int*)arg);
    printf("Hello from thread %d!\n", thread_id);
    pthread_exit(NULL);
}
 
int main() {
    pthread_t thread1, thread2;
    int thread_ids[2] = {1, 2};
 
    // Créer le premier thread
    pthread_create(&thread1, NULL, task, &thread_ids[0]);
 
    // Créer le deuxiÚme thread
    pthread_create(&thread2, NULL, task, &thread_ids[1]);
 
    // Attendre la fin des threads (facultatif)
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
 
    printf("Main thread exiting.\n");
    return 0;
}

Voir Attendre un thread pour la fonction pthread_join


Mise en Ɠuvre (Java)

Ressource utile : www.w3schools.com

Pour utiliser des threads en Java, il faut :

  1. Déclarer une classe qui hérite de Thread.
  2. Implémenter la méthode public void run(), avec le code à exécuter pour le thread.

Afin de démarrer le thread, il faut appeler la méthode start(). On peut attendre la fin du thread avec join()

Exemple :

class Task implements Runnable {
    private int threadId;
 
    public Task(int threadId) {
        this.threadId = threadId;
    }
        
    @Override
    public void run() {
        System.out.println("Hello from thread " + threadId + "!");
    }
}
    
public class ThreadExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Task(1));
        Thread thread2 = new Thread(new Task(2));
 
        thread1.start();
        thread2.start();
 
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        System.out.println("Main thread exiting.");
    }
}

Types de threads

Threads systĂšmes

Les threads systĂšmes sont directement gĂ©rĂ©s par le systĂšme d’exploitation. Chaque thread systĂšme correspond Ă  une entitĂ© d’ordonnancement distincte pour le noyau. Cela signifie que le systĂšme d’exploitation alloue des ressources (comme du temps CPU) Ă  chaque thread de maniĂšre indĂ©pendante.

Exemple

Imaginez une voiture avec plusieurs passagers. Chaque passager reprĂ©sente un thread systĂšme. Le conducteur (le systĂšme d’exploitation) dĂ©cide Ă  quel moment chaque passager (thread) peut regarder par la fenĂȘtre (utiliser le processeur). Le conducteur peut dĂ©cider de changer de passager Ă  tout moment, en fonction de diffĂ©rents critĂšres (prioritĂ©, temps d’exĂ©cution restant, etc.).

Avantages

  • Flexibilité : Grande flexibilitĂ© dans la gestion des threads, car le systĂšme d’exploitation offre un contrĂŽle fin sur l’ordonnancement.
  • Performance : Les threads systĂšmes peuvent tirer pleinement parti des capacitĂ©s multicƓur des processeurs modernes.

Inconvénients

  • CoĂ»t : La crĂ©ation et la gestion de threads systĂšmes peuvent ĂȘtre coĂ»teuses en termes de ressources systĂšme.
  • Complexité : La programmation avec des threads systĂšmes peut ĂȘtre plus complexe, car il faut gĂ©rer explicitement la synchronisation et les communications entre les threads.

‘Green’ threads

Les ‘green’ threads sont des threads gĂ©rĂ©s par un ordonnanceur utilisateur, c’est-Ă -dire un logiciel qui s’exĂ©cute au sein de l’application. Cet ordonnanceur dĂ©cide lui-mĂȘme quand passer d’un thread Ă  l’autre, sans impliquer directement le systĂšme d’exploitation.

Exemple

Imaginez une troupe de théùtre oĂč chaque acteur reprĂ©sente un ‘green’ thread. Le metteur en scĂšne (l’ordonnanceur utilisateur) dĂ©cide de l’ordre dans lequel les acteurs entrent en scĂšne et de la durĂ©e de leur intervention. Le systĂšme d’exploitation (la salle de théùtre) ne s’occupe que de fournir la scĂšne et les lumiĂšres, sans se soucier de la coordination des acteurs.

Avantages

  • LĂ©gĂšreté : Les ‘green’ threads sont gĂ©nĂ©ralement plus lĂ©gers Ă  crĂ©er et Ă  gĂ©rer que les threads systĂšmes, car ils ne nĂ©cessitent pas l’intervention du systĂšme d’exploitation Ă  chaque commutation de contexte.
  • Flexibilité : L’ordonnanceur utilisateur peut implĂ©menter des stratĂ©gies d’ordonnancement spĂ©cifiques aux besoins de l’application.

Inconvénients

  • Performance : Les ‘green’ threads peuvent ĂȘtre moins performants que les threads systĂšmes, car l’ordonnanceur utilisateur peut ne pas ĂȘtre aussi optimisĂ© que celui du systĂšme d’exploitation.
  • Blocage : Si un ‘green’ thread est bloquĂ© (par exemple, en attendant une entrĂ©e/sortie), tous les autres ‘green’ threads du mĂȘme processus peuvent ĂȘtre bloquĂ©s, parce que l’ordonnanceur utilisateur ne peut pas passer Ă  un thread systĂšme.

Partage de données et la data-concurrence

Partager les mĂȘmes donnĂ©es entre plusieurs threads peut ĂȘtre TRÈS dangereux.

Race condition

Une race condition survient lorsqu’un programme multithread accĂšde Ă  des donnĂ©es partagĂ©es de maniĂšre non atomique et que le rĂ©sultat final dĂ©pend de l’ordre d’exĂ©cution imprĂ©visible de ces accĂšs. En d’autres termes, c’est une situation oĂč deux ou plusieurs threads tentent de modifier une mĂȘme donnĂ©e en mĂȘme temps, et le rĂ©sultat final dĂ©pend de l’ordre dans lequel ces modifications sont effectuĂ©es.

Imaginez un compteur partagĂ© par deux threads. Chaque thread incrĂ©mente ce compteur plusieurs fois. Si les deux threads lisent la valeur actuelle du compteur en mĂȘme temps, puis l’incrĂ©mentent, et enfin Ă©crivent la nouvelle valeur, il est possible que l’une des incrĂ©mentations soit perdue. Par exemple, si les deux threads lisent la valeur 0, l’incrĂ©mentent et Ă©crivent la valeur 1, le compteur final affichera 1 au lieu de 2, car l’une des incrĂ©mentations a Ă©tĂ© Ă©crasĂ©e.

Pourquoi c’est problĂ©matique ?

  • RĂ©sultats imprĂ©visibles : Le comportement du programme devient non dĂ©terministe, c’est-Ă -dire qu’il peut produire des rĂ©sultats diffĂ©rents Ă  chaque exĂ©cution, mĂȘme avec les mĂȘmes entrĂ©es.
  • DifficultĂ© de dĂ©bogage : Les race-conditions sont souvent difficiles Ă  reproduire et Ă  corriger, car elles dĂ©pendent de facteurs tels que le temps d’exĂ©cution et le planificateur du systĂšme.
  • Bugs subtils : Les erreurs causĂ©es par les race-conditions peuvent ĂȘtre trĂšs subtiles et difficiles Ă  dĂ©tecter, surtout dans des programmes complexes.

Exemple

Imaginons un programme qui va ouvrir deux threads, le premier va ajouter 5 Ă  v et le deuxiĂšme va ajouter 10 Ă  v On aura alors la suite d’exĂ©cution suivante :

Thread 1Thread 2
- lire la valeur de v- lire la valeur de v
- Ajouter 5- Ajouter 10
- Ecrire le résultat- Ecrire le résultat

Lorsque plusieurs threads accĂšdent Ă  une mĂȘme ressource partagĂ©e (ici, la variable v) et la modifient, il peut se produire des conditions de course. Ces conditions de course peuvent entraĂźner des rĂ©sultats incorrects, des pertes de donnĂ©es, ou mĂȘme des plantages de l’application.


Deadlock

Un deadlock (impasse en français) est une situation dans laquelle deux ou plusieurs threads sont bloquĂ©s indĂ©finiment, chacun attendant qu’un autre libĂšre une ressource qu’il dĂ©tient. C’est une forme de blocage mutuel qui peut paralyser complĂštement une application multithread.

Exemple concret

Imaginez deux philosophes assis autour d’une table ronde, chacun avec une fourchette dans chaque main. Pour manger, un philosophe a besoin des deux fourchettes Ă  cĂŽtĂ© de lui. Si chaque philosophe prend sa fourchette gauche, puis attend que sa fourchette droite se libĂšre, aucun d’eux ne pourra jamais manger, car ils attendent tous que l’autre libĂšre sa fourchette.

Les quatre conditions nĂ©cessaires pour qu’un deadlock se produisent :

  1. Exclusion mutuelle : Une ressource ne peut ĂȘtre utilisĂ©e que par un seul thread Ă  la fois.
  2. Attente circulaire : Un ensemble de threads attendent chacun une ressource dĂ©tenue par le thread suivant de l’ensemble.
  3. Non-privation : Un thread ne peut pas se faire retirer une ressource qu’il dĂ©tient.
  4. Attente : Un thread doit attendre une ressource détenue par un autre thread.

ConsĂ©quences d’un deadlock:

  • Blocage de l’application : L’application peut devenir complĂštement inutilisable si les threads bloquĂ©s sont essentiels Ă  son fonctionnement.
  • Perte de ressources : Les ressources dĂ©tenues par les threads bloquĂ©s sont inutilisables jusqu’à ce que le deadlock soit rĂ©solu.
  • DifficultĂ© de dĂ©bogage : Les deadlocks peuvent ĂȘtre difficiles Ă  dĂ©tecter et Ă  reproduire, car ils dĂ©pendent souvent de l’ordre d’exĂ©cution des threads.

Mécanismes de protection face aux deadlocks et aux race-conditions

Je dois m'en occuper, voici quelques ressources en attendant