L'algorithme de la boulangerie ((en)Lamport's bakery algorithm) est un algorithme d'exclusion mutuelle découvert par Leslie Lamport, dans le cadre général de machines multi-processeurs à mémoire partagée ne fournissant aucune opération atomique.
Dans sa forme originelle, il utilise de l'attente active avant l'entrée en section critique.
L'algorithme de la boulangerie peut être utilisé afin de réaliser une exclusion mutuelle sur toute machine multi-processeur, y compris celles qui ne fournissent pas d'opérations atomiques, ou qui en fournissent de simples ne permettant de réaliser qu'une seule opération mémoire (lecture ou écriture) à la fois.
Cependant, toutes les architectures modernes proposent des opérations atomiques combinant à la fois la lecture et l'écriture en une seule opération (comme Test And Set, Fetch And Add ou Compare And Swap). Ces dernières autorisent des implémentations de l'exclusion mutuelle plus efficaces.
L'algorithme de la boulangerie présente donc aujourd'hui principalement un intérêt théorique et pédagogique (lire la section pour plus de détails).
L'algorithme garantit l'exclusion mutuelle de N fils d'exécution, N étant fixé à l'avance.
Les fils doivent posséder des identifiants (habituellement, un entier) comparables (c'est-à-dire éléments d'un ensemble munis d'une relation d'ordre, supposée totale).
L'algorithme requiert également une zone de mémoire partagée servant à stocker deux tableaux comportant N éléments:
La valeur particulière 0 indique, dans ce dernier tableau, qu'un fil ne souhaite pas entrer en section critique. Les autres valeurs représentent des numéros de ticket potentiels.
Il n'est pas nécessaire que les opérations de lecture et d'écriture sur les tableaux partagés soient réalisées de manière atomique. Pour faciliter la compréhension, le lecteur peut dans un premier temps supposer que ces opérations sont atomiques, puis se référer à la section pour obtenir des précisions sur la validité de ce point.
L'algorithme ne présuppose rien sur la vitesse d'exécution relative des différents fils. Il garantit qu'un seul fil exécute la section critique à la fois. Il n'introduit pas d'interblocage ou de famine.
Avant toute utilisation de l'algorithme, les tableaux partagés doivent être initialisés comme suit.
Initialiser toutes les cases de COMPTEUR à 0. Initialiser toutes les cases de CHOIX à 0 (ou FAUX).
Le code suivant est exécuté par un fil souhaitant entrer en section critique. ID désigne son identifiant.
Cette portion de code vise à l'attribution d'un ticket. Le principe en est de regarder tous les numéros déjà attribués et de s'en attribuer un nouveau plus grand que les autres.
' Début de la phase d'attribution d'un ticket CHOIX[ID] = 1 MAX = 0 ' Calcul de la valeur maximale des tickets des autres threads POUR J = 0 À N - 1 FAIRE CJ = COMPTEUR[J] SI MAX < CJ ALORS MAX = CJ FIN SI FIN POUR J ' Attribution du nouveau ticket COMPTEUR [ID] = MAX + 1 ' Fin de la phase d'attribution du ticket CHOIX[ID] = 0
Cependant, il est possible que plusieurs fils d'exécution exécutent le code de cette phase de manière concurrente. Ils peuvent alors réaliser le même calcul du maximum et s'attribuer le même ticket. Ce cas est pris en compte lors de la phase suivante.
En toute généralité, si on ne suppose pas que les lectures et écritures sont atomiques, il est même possible que des fils exécutant de manière concurrente la première phase obtiennent des valeurs de ticket différentes. Ces valeurs sont cependant nécessairement plus grandes que celles des tickets d'autres fils ayant atteint la deuxième phase avant que ne commence l'attribution des tickets pour les premiers.
Cette phase correspond, dans l'analogie développée plus haut, à l'attente de son tour. Le principe est d'examiner tous les autres fils d'exécution et de tester s'ils ont un numéro de ticket inférieur, auquel cas ils doivent passer dans la section critique en premier.
' Boucle sur tous les fils d'exécution POUR J = 0 À N - 1 FAIRE ' On attend que le fil considéré (J) ait fini de s'attribuer un ticket. TANT QUE CHOIX[J] == 1 FAIRE ' (1) RIEN FIN TANT QUE ' On attend que le fil courant (ID) devienne plus prioritaire que le fil considéré (J). TANT QUE COMPTEUR[J] <> 0 ET ' (2) (COMPTEUR[J] < COMPTEUR[ID] OU ' (3) (COMPTEUR[J] == COMPTEUR[ID] ET J < ID)) ' (4) FAIRE RIEN FIN TANT QUE FIN POUR J
La comparaison des tickets a lieu à la ligne indiquée par (3). Comme expliqué précédemment, il est possible que deux fils d'exécution s'attribuent le même ticket. Dans ce cas, le plus prioritaire est celui qui possède l'identifiant le plus petit (voir la ligne (4)). La priorité est donc évaluée selon l'ordre lexicographique sur les couples (COMPTEUR[J], J). Seuls les fils souhaitant réellement entrer en section critique sont pris en compte (voir la ligne (2)).
Un point essentiel au bon fonctionnement de l'algorithme, et vraisemblablement l'un des plus difficiles à comprendre, est l'utilisation du tableau CHOIX, afin d'éviter de prendre en compte par erreur une ancienne valeur de ticket pour les autres fils (ligne (1)). Supposons que 2 fils J et ID, avec J < ID, exécutent la boucle de la première phase de manière concurrente et qu'ils se voient attribuer le même numéro de ticket. Pour simplifier, nous considèrerons que tous les autres fils sont hors de la section critique et ne cherchent pas à y entrer. Supposons également que, alors que le fil ID exécute la deuxième phase, le fil J n'a pas encore exécuté l'opération de mise à jour de son ticket dans la première phase (COMPTEUR [J] = MAX + 1 n'a pas été exécutée). Supposons enfin que l'on a retiré la boucle d'attente sur la valeur de CHOIX[J] (ligne (1)). Dans ce cas, le fil ID lit COMPTEUR[J] (ligne (2)), qui vaut 0, sans prendre en compte la valeur de CHOIX[J]. Cette valeur de 0 est censée signifier que le fil J est hors de la section critique et ne cherche pas à y entrer. Le fil ID en déduit qu'il est prioritaire par rapport à J et entre dans la section critique. Le fil J, poursuivant son exécution, met à jour COMPTEUR[J] et entre dans la deuxième phase. Il remarque alors qu'il a le même numéro de ticket que le fil ID et compare leurs identifiants. Comme J < ID, J est prioritaire. Il finit donc par entrer dans la section critique. Finalement, dans ce cas, les fils J et ID peuvent tous les deux entrer dans la section critique, ce que l'algorithme cherche justement à éviter.
À la sortie de la section critique, le fil courant (ID) exécute simplement le code ci-dessous. COMPTEUR[ID] est remis à zéro, indiquant aux autres fils que le fil ID n'est plus en section critique et ne cherche pas à y accéder.
COMPTEUR[ID] = 0