Algorithme de Peterson

Un article de Wikipédia, l'encyclopédie libre.

En informatique, l'algorithme de Peterson est un algorithme d'exclusion mutuelle pour la programmation concurrente. Cet algorithme est basé sur une approche par attente active et est garanti d'être sans famine et sans interblocage[1]. Il est constitué de deux parties : le protocole d'entrée dans la section critique et le protocole de sortie. L'algorithme présenté est une version pouvant fonctionner avec deux threads. Il a été publié par Gary Peterson en 1981.

Description de l'algorithme[modifier | modifier le code]

Le principe de base de l'algorithme de Peterson est que chaque thread aura accès à son tour à la section critique, et qu'un thread ne peut entrer dans la section critique que lorsque celle-ci est libre.

Chaque thread dispose d'un identifiant unique. Par convention, nous considérons que les deux threads utilisés porteront les identifiants 0 et 1. Dans son implémentation de base avec 2 processus, l'algorithme utilise un tableau de deux valeurs booléennes pour indiquer si l'un des threads est actuellement intéressé d'entrer dans la section critique. Il utilise également une autre variable pour indiquer quel thread a actuellement accès à la zone critique.

La particularité importante de cet algorithme est qu'il utilise deux variables différentes pour contrôler l'accès la zone critique.

Algorithme initial pour 2 processus[modifier | modifier le code]

Ci-dessous veut_entrer est un tableau de deux booléens, indiquant pour chaque processus (0 et 1) s'il veut entrer en section critique. La variable tour indique à qui est le tour d'exécuter la section critique.

// Initialisation
bool veut_entrer[2];
veut_entrer[0] = false;
veut_entrer[1] = false;
int tour;
// Execution Thread n°0
T0: veut_entrer[0] = true;
    tour = 1;
    while (veut_entrer[1] && tour == 1) {} // attente
    // Début de la section critique
    ...
    // fin de la section critique
    veut_entrer[0] = false;
// Execution Thread n°1
P1: veut_entrer[1] = true;
    tour = 0;
    while (veut_entrer[0] && tour == 0) {} // attente
    // Début de la section critique
    ...
    // fin de la section critique
    veut_entrer[1] = false;

Exemple en Java[modifier | modifier le code]

Voici le pseudo-code de l'algorithme écrit en Java[1]. Les deux threads possèdent comme identifiant 0 et 1, ils peuvent obtenir leur identifiant en appelant ThreadID.get().

class Peterson implements Lock {
    // index du thread local, vaut 0 ou 1
    private boolean[] drapeau = new boolean[2];
    private int victime;
    public void lock() {
        int i = ThreadID.get();
        int j = 1 - i;
        drapeau[i] = true;                      // je suis intéressé
        victime = i;                            // il va en premier
        while (drapeau[j] && victime == i) {}   // j'attends
    }
    public void unlock() {
        int i = ThreadID.get();
        drapeau[i] = false;                     // je ne suis plus intéressé
    }
}

Exemple d'application en C (avec la librairie 'pthread')[modifier | modifier le code]

#include <stdio.h>
#include <pthread.h>

#define PTHREAD_FUNCTION(fct) (void *(*)(void *))fct

#define FALSE 0
#define  TRUE 1

// Utilisons l'algorithme de Peterson, 1981
typedef struct {
    // On a besoin d'un tableau de la même taille 
    // que le nombre de thread qu'on doit gérer
    int veut_entrer[2];
    int tour;
    int shared_value;
} Peterson_two_thread_struct;

void thread_num_zero(Peterson_two_thread_struct *ptt)
{
    ptt->veut_entrer[0] = TRUE;
    ptt->tour = 1;
    
    while (ptt->veut_entrer[1] && ptt->tour == 1) {} // On attends
    
    // Section critique
    for (int i = 0; i < 100; i++) {
        ptt->shared_value++;
        printf("[0] shared_value = %i\n", ptt->shared_value);
    }
    
    // Fin de la section critique
    ptt->veut_entrer[0] = FALSE;
}

void thread_num_one(Peterson_two_thread_struct *ptt)
{
    ptt->veut_entrer[1] = TRUE;
    ptt->tour = 0;
    
    while (ptt->veut_entrer[0] && ptt->tour == 0) {} // On attends
    
    // Section critique
    for (int i = 0; i < 100; i++) {
        ptt->shared_value++;
        printf("[1] shared_value = %i\n", ptt->shared_value);
    }
    
    // Fin de la section critique
    ptt->veut_entrer[1] = FALSE;
}

int main(int argc, const char * argv[]) {
    
    pthread_t pthread_0, pthread_1;

    Peterson_two_thread_struct ptt_struct;
    ptt_struct.veut_entrer[0] = FALSE;
    ptt_struct.veut_entrer[1] = FALSE;
    ptt_struct.shared_value = 0;
    
    printf("START : shared_value = %i\n", ptt_struct.shared_value);
    
    int ret0 = pthread_create(&pthread_0, NULL, PTHREAD_FUNCTION(thread_num_zero), &ptt_struct);
    int ret1 = pthread_create(&pthread_1, NULL, PTHREAD_FUNCTION(thread_num_one), &ptt_struct);
    
    // Si le système ne peut plus créer de threads (lisez le manuel)
    if(ret0 != 0 || ret1 != 0)
    {
        printf("Error\n");
        printf("Thread \"thread_num_zero\" error : %i \n", ret0);
        printf("Thread \"thread_num_one\" error : %i \n", ret1);
        return -1;
    }
    
    pthread_join(pthread_0, NULL);
    pthread_join(pthread_1, NULL);
    
    printf("END : shared_value = %i\n", ptt_struct.shared_value);
    
    return 0;
}

Propriétés de l'algorithme[modifier | modifier le code]

L'algorithme satisfait plusieurs propriétés tel que l'exclusion mutuelle, l'absence de famine et d'interblocage.

Voici les développements qui prouvent ces propriétés. Remarque: elles s’appuient sur le code de l'exemple en Java.

Exclusion mutuelle[modifier | modifier le code]

L'algorithme de Peterson satisfait la propriété de l'exclusion mutuelle.

Preuve. Supposons qu'il ne satisfasse pas l'exclusion mutuelle. Considérons les dernières exécutions des threads A et B avant qu'elles ne s'entrecroisent au niveau de leur section critique, respectivement CSA et CSB. En inspectant le code, on remarque que :

writeA(drapeau[A] = vrai) ⇒ writeA(victime = A) ⇒ readA(drapeau[B]) ⇒ readA(victime) ⇒ CSA, (1) writeB(drapeau[B] = vrai) ⇒ writeB(victime = B) ⇒ readB(drapeau[A]) ⇒ readB(victime) ⇒ CSB (2)

Supposons que A est le dernier thread a écrire dans le champ victime, cette supposition ayant peu d'impact sur la parte de généralité de l'algorithme. Ainsi, on peut écrire :

writeB(victime = B) ⇒ writeA(victime = A) (3)

L'équation (3) suppose que A a observé la variable victime dans (1). Comme A est entré dans sa zone critique, par conséquent A a forcément observé que drapeau[B] était faux. Nous avons donc :

writeA(victime = A) ⇒ readA(drapeau[B] == faux) (4)

En combinant les équations (2) et (4) ensemble, on obtient :

writeB(drapeau[B] = vrai) ⇒ writeB(victime= B) ⇒ writeA(victime= A) ⇒ readA(drapeau[B] == faux). (5)

Via les règles de transitivité de l'implication (⇒), on peut réécrire (5) en :

writeB(drapeau[B] = vrai) ⇒ readA(drapeau[B] == faux).

Ce qui constitue une contradiction. En effet, aucune autre écriture de drapeau[B] n'a pu être effectuée avant les exécutions de la section critique. Par conséquent l'hypothèse de base est erronée et l'algorithme de Peterson satisfait bien la propriété de l'exclusion mutuelle. □

Absence de famine[modifier | modifier le code]

L'algorithme de Peterson satisfait la propriété de l'absence de famine.

Preuve. Supposons que cela ne soit pas le cas. Il y a donc au moins un des deux threads qui s'exécute pour toujours sans jamais entrer dans la section critique. Supposons que cela soit A (la preuve est similaire si on supposait que cela soit B). A exécute donc la ligne while (drapeau[j] && victime == i) {} en boucle, et attendant donc que soit drapeau[j] devienne faux, soit que victime soit égale à B.

Que fait B tandis que A exécute cette ligne de code ? Il doit être en train d'entrer et sortir de la zone critique de manière répétée. Cela veut donc dire que B met la variable victime à B à chaque fois qu'il veut entrer dans la section critique. Par conséquent, la variable victime est toujours égale à B et ne change jamais de valeur. Si c'est le cas, A peut alors sortir de la boucle puisqu'une des deux conditions est remplie. C'est donc une contradiction.

Si ce n'est pas ça, c'est alors parce que B est également coincée dans la boucle en attendant de pouvoir entrer dans la zone critique, attendant que soit flag[A] devienne faux soit victime soit mise à A. Mais victime ne peut pas valoir à la fois A et B. C'est donc également une contradiction. Par conséquent, l'hypothèse de base est fausse et l'algorithme satisfait bien la propriété de l'absence de famine. □

Absence d'interblocage (deadlock)[modifier | modifier le code]

L'algorithme de Peterson satisfait la propriété de l'absence d'interblocage.

Preuve. Comme il a déjà démontré que l'algorithme satisfait la propriété de l'absence de famine, et que la propriété de l'absence de famine est une propriété plus forte que l'absence d'interblocage, alors l'algorithme est garanti de ne pas avoir d'interblocage. □

Voir aussi[modifier | modifier le code]

Références[modifier | modifier le code]

  1. a et b (en) Maurice Herlihy, Nir Shavit, Victor Luchangco et Michael Spear, The art of multiprocessor programming, Morgan Kaufmann, 2e éd. (ISBN 978-0-12-415950-1), p. 27-28