SE2 : Introduction au multithreading

SE2: Système d'exploitation  

Printemps 2024

Instructrices: GUEALIA Nadia

The Stanford University logo
A graphic "words that will never be the same after CS110".  It has various drawings below it of: fork (labeled "fork"), needle and thread (labeled "thread"), a bundled baby (labeled "child"), a shovel (labeled "farm"), a seashell (labeled "shell"), an open padlock (labeled "lock"), a knife (labeled "kill"), a paper document (labeled "permit"), a dead face with two Xs for eyes and a stuck-out tongue (labeled "zombie"), a pipe (labeled "pipe") and a hat (labeled "workers").  Below the diagram is the author's name and a smiley face: "Ecy :)"

Illustration courtesy of Ecy King, CS110 Champion, Spring 2021

Comment pouvons-nous avoir de la concurrence au sein d’un même processus ?

Objectifs

  • Découvrez comment les threads permettent la concurrence au sein d'un même processus
  • Comprenez les différences entre les threads et les processus
  • Découvrez comment le partage du même espace d'adressage virtuel entre différent threads peut être dangereux

From Processes to Threads

  • Le multitraitement (multiprocessing) nous a permis de générer d'autres processus pour effectuer des tâches ou exécuter des programmes
  • Puissant ; peut exécuter/attendre d'autres programmes, sécurisé (espace mémoire séparé), communiquer avec des tuyaux (pipes) et des signaux
  • Mais limité ; la communication interprocessus est lourde, il est difficile de partager des données/de coordonner
  • Existe-t-il un autre moyen d’obtenir une concurrence au-delà du multitraitement qui gère ces compromis différemment ?

Nous pouvons avoir une concurrence au sein d'un même processus en utilisant des threads : des séquences d'exécution indépendantes au sein d'un même processus.

  • Les threads nous permettent d'exécuter simultanément plusieurs fonctions de notre programme
  • Le multithreading est très courant pour paralléliser les tâches, en particulier sur plusieurs cores
  • En C: générez un thread en utilisant pthread_create et spécifiez la fonction que vous souhaitez que le thread exécute (en passant éventuellement des paramètres !)
  • Le gestionnaire de threads bascule entre les threads en cours d'exécution comme le scheduler  du système d'exploitation bascule entre les processus en cours d'exécution
  • Chaque thread fonctionne dans le même processus, ils partagent donc le même espace d'adressage virtuel (!) (Variables globales, texte, données et segments de tas)
  • Le segment de pile des processus « stack » est divisé en « ministack » pour chaque thread.
  • Il existe de nombreuses similitudes entre les threads et les processus ; en fait, les threads sont souvent appelés processus légers "lightweight processes" .

Multithreading

Processus:

  • isoler les espaces d'adressage virtuels (bon : sécurité et stabilité, mauvais : partage d'informations plus difficile)
  • peut exécuter facilement des programmes externes (fork-exec) (bien)
  • plus difficile de coordonner plusieurs tâches au sein du même programme (mauvais)

Threads:

  • partager l'espace d'adressage virtuel (mauvais : sécurité et stabilité, bon : partage d'informations plus facile)
  • ne peut pas exécuter facilement des programmes externes (mauvais)
  • plus facile de coordonner plusieurs tâches au sein d'un même programme (bien)

Threads vs. Processus

C thread

Un thread peut être généré pour exécuter la fonction spécifiée avec les arguments donnés.

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
//Compile and link with -pthread.
  • La fonction pthread_create() démarre un nouveau thread dans le processus appelant. Le nouveau thread démarre l'exécution en appelant start_routine(); arg est passé comme seul argument de start_routine().
  • start_routine(): la fonction que le thread doit exécuter de manière asynchrone
  • arg: une liste d'arguments (zéro ou n'importe quelle longueur ) à passer à la fonction lors de l'exécution
  • Une fois créer le thread peut s'exécuter à tout moment !

C thread

Pour plusieurs threads, nous devons attendre un thread spécifique à la fois :

thread treads[5];

...

for (size_t i = 0; i < 5; i++) {
	    pthread_join(treads[i], NULL);

Pour attendre la fin d'un thread, utilisez la fonction pthread_join :

static void * threadFunc(void *arg)
{// instructions
    return NULL;
}
//
pthread_t mythread;
pthread_create(&mythread, NULL,threadFunc, NULL);
... // do some work
// Wait for thread to finish (blocks)
pthread_join(mythread, NULL);

C thread

Le nouveau thread se termine de l'une des manières suivantes :
  • Il appelle pthread_exit(3), en spécifiant une valeur d'état de sortie qui est disponible pour un autre thread dans le même processus qui appelle pthread_join(3).		          
              
  • Il revient de start_routine(). Cela équivaut à appeler pthread_exit(3) avec la valeur fournie dans l'instruction return.
    
  • Il est annulé (voir pthread_cancel(3)).
    
  • L'un des threads du processus appelle exit(3), ou le thread principal effectue un retour depuis main(). Cela provoque la fin de tous les threads du processus.
    

Terminaison d'un thread:

Notre premier programme Thread

#include <stdio.h>
#include <pthread.h>         // for C thread support


static const int kNumFriends = 6;

static void *                   /* Loop 'arg' times incrementing 'glob' */
greeting(void *arg)
{
    printf("Hello, world!\n");
    return NULL;
}

int main(int argc, char *argv[]) {
  printf("Let's hear from  %d threads \n",kNumFriends);
   

   // declare array of empty thread handles
  pthread_t friends[kNumFriends];

  // Spawn threads
  for (size_t i = 0; i < kNumFriends; i++) {
      pthread_create(&friends[i], NULL, greeting, NULL);
  }

  // Wait for threads
  for (size_t i = 0; i < kNumFriends; i++) {
     pthread_join(friends[i], NULL);
  }

  printf("Everyone's said hello!\n");
  return 0;
}

Our First Threads Program

Our First Threads Program

#include <stdio.h>
#include <pthread.h>         // for C thread support


static const int kNumFriends = 6;

static void *                   /* Loop 'arg' times incrementing 'glob' */
greeting(void *arg)
{
    int * iptr =(int*)arg;
    printf("Hello, world! I am thread %i \n",*iptr);
    return NULL;
}

int main(int argc, char *argv[]) {
  printf("Let's hear from  %d threads \n",kNumFriends);
   

   // declare array of empty thread handles
  pthread_t friends[kNumFriends];

  // Spawn threads
  for (size_t i = 0; i < kNumFriends; i++) {
      pthread_create(&friends[i], NULL, greeting, &i);
  }

  // Wait for threads
  for (size_t i = 0; i < kNumFriends; i++) {
     pthread_join(friends[i], NULL);
  }

  printf("Everyone's said hello!\n");
  return 0;
}

Our First Threads Program

C thread

// declare array of empty thread handles
  pthread_t friends[kNumFriends];

  // Spawn threads
  for (size_t i = 0; i < kNumFriends; i++) {
      pthread_create(&friends[i], NULL, greeting, &i);
  }

Nous pouvons créer un tableau de threads comme suit :

Race Conditions (Conditions de course)

  • Comme pour les processus, les threads peuvent s’exécuter dans des ordres imprévisibles.
  • Une condition de course est un ordre imprévisible d'évènements où certains ordres peuvent provoquer un comportement indésirable.
  • Une fonction thread-safe est une fonction qui s'exécutera toujours correctement, même lorsqu'elle est appelée simultanément à partir de plusieurs threads.
  • printf n'est pas une fonction thread-safe. Cela signifie par exemple que les affichage peuvent être entrelacées !

Threads Program(en c++)

static const size_t kNumFriends = 6;

static void greeting(size_t i) {
    cout << oslock << "Hello, world! I am thread " << i << endl << osunlock;
}

int main(int argc, char *argv[]) {
  cout << "Let's hear from " << kNumFriends << " threads." << endl;  

   // declare array of empty thread handles
  thread friends[kNumFriends];

  // Spawn threads
  for (size_t i = 0; i < kNumFriends; i++) {
      friends[i] = thread(greeting, i); 
  }

  // Wait for threads
  for (size_t i = 0; i < kNumFriends; i++) {
     friends[i].join();    
  }

  cout << "Everyone's said hello!" << endl;
  return 0;
}

Les threads partagent la mémoire

  • Contrairement aux processus parents/enfants, les threads s'exécutent dans le même espace d'adressage virtuel!
  • Cela signifie que nous pouvons par exemple transmettre des paramètres par référence et permettre à tous les threads d'y accéder/de les modifier !
  • Pour passer une valeur par référence avec pthread_create, nous devons utiliser  le quatrième arguments:
greeting(void *arg)
{
    int * iptr =(int*)arg;

...

}
// Spawn threads
  for (size_t i = 0; i < kNumFriends; i++) {
      pthread_create(&friends[i], NULL, greeting, &i);
  }

Threads Share Memory

  for (size_t i = 0; i < kNumFriends; i++) {
      pthread_create(&friends[i], NULL, greeting, &i);
  }

_start

greeting

main

argc

argv

i

args

args

args

args

args

args

piles de threads

main stack

Ici, nous pouvons simplement passer par copie. Mais faites attention aux conséquences de la mémoire partagée !

Threads Share Memory

Introduction to Multithreading

By NadGy

Introduction to Multithreading

  • 100