Vérification de systèmes concurrents : systèmes de transitions et programmation multi-threads
Afficher ou masquer la section
Le sujet porte sur la vérification algorithmique de propriétés de systèmes de transitions, un cadre classique pour modéliser des exécutions infinies. Il introduit les propriétés de sûreté et l'ultime invariance, avec l'algorithme des parcours en profondeur imbriqués à programmer en OCaml, puis la programmation concurrente en langage C avec la bibliothèque pthread, avant d'étudier comment des notions comme l'indépendance d'actions et les ensembles persistants permettent de réduire l'explosion combinatoire lors de la vérification de systèmes concurrents.
1Partie I : systèmes de transitionsDéfinitions des systèmes de transitions, des exécutions infinies, des propriétés de trace, des invariants et des propriétés de sûreté régulières, avec une première fonction OCaml d'accessibilité.
2Partie II : ultime invarianceÉtude de la propriété d'ultime invariance et démonstration puis implémentation en OCaml de l'algorithme des parcours en profondeur imbriqués pour la vérifier efficacement.
3Partie III : programmation multi-threads en CRappels sur la bibliothèque pthread, analyse d'un programme concurrent comportant une erreur d'accès concurrent, puis écriture d'un programme multi-threads calculant un maximum réparti.
4Partie IV : systèmes de transitions concurrentsModélisation de programmes C par des systèmes de transitions, définition de l'indépendance entre actions, des ensembles persistants et de la technique de mise en veille pour réduire l'exploration des entrelacements concurrents.
Ces sujets peuvent vous intéresser
Pas encore de corrigé pour ce sujet : voici des sujets proches corrigés.
Durée : 4 heures
L'utilisation des calculatrices n'est pas autorisée pour cette épreuve
Vérification de systèmes concurrents
Le sujet comporte 17 pages et 24 questions.
Vue d'ensemble du sujet.
Les systèmes de transitions constituent un cadre classique pour la modélisation et l'analyse des systèmes en informatique. On peut les voir comme une variante des automates finis. Cependant, les automates sont utilisés pour lire des mots finis (et les accepter ou non) tandis que les systèmes de transitions sont utilisés pour produire des exécutions infinies, qu'on va ensuite analyser.
Dans les parties I et II de ce sujet, on s'intéresse à la vérification algorithmique de certaines propriétés des exécutions infinies d'un système de transitions. En particulier, on verra en partie II l'algorithme des parcours en profondeur imbriqués pour la vérification des propriétés d'ultime invariance. Ces deux parties comportent des questions de programmation OCaml.
Dans les parties III et IV, on s'intéresse plus particulièrement aux systèmes concurrents. La partie III porte sur la programmation concurrente en langage C, rappelant l'utilisation de la bibliothèque pthread, avec des questions d'analyse et d'écriture de programmes. On voit en partie IV comment de tels programmes peuvent être modélisés par des systèmes de transitions, et l'on introduit des notions classiques de théorie de la concurrence permettant d'analyser ces systèmes en évitant autant que possible l'explosion combinatoire induite par les entrelacements d'instructions concurrentes.
Les définitions de la partie I sont utiles dans tout le sujet. La partie II est indépendante du reste du sujet. La partie III fournit des rappels utiles et un exemple de programme utilisé en partie IV.
Rappels
Si E est un ensemble, on note 2^E l'ensemble de ses parties.
Si E et F sont deux ensembles, on rappelle qu'une relation R entre E et F est une partie de E × F, que l'on appelle parfois le graphe de la relation. Deux éléments x ∈ E et y ∈ F sont en relation pour R quand (x, y) ∈ R; on note alors xRy. Deux relations entre les mêmes ensembles coïncident lorsque leurs graphes sont égaux. On parlera parfois de plus petite relation entre E et F vérifiant certaines propriétés. Cette formulation est à comprendre au sens de l'inclusion des graphes : une relation R est plus petite que R^′ quand le graphe de la seconde contient celui de la première, i.e. R ⊆ R^′.
On rappelle que Σ^∗ est l'ensemble des mots finis sur Σ. La longueur d'un mot u ∈ Σ^∗ sera notée |u| : c'est le nombre de lettres dans u. On note ε l'unique mot de longueur nulle. Pour w ∈ Σ^∗ et i < |w|, on note w_i la lettre d'indice i dans w, en numérotant à partir de 0 : on a donc w = w_0…w_(|w| − 1).
On note Σ^ω l'ensemble des mots infinis sur Σ, défini comme l'ensemble des suites infinies (a_i)_(i ∈ ℕ) avec a_i ∈ Σ pour tout i ∈ ℕ. Si w est un mot infini et i ∈ ℕ, on note w_i la lettre d'indice i dans w, c'est-à-dire a_i si w = (a_i)_(i ∈ ℕ). La concaténation d'un mot fini u ∈ Σ^∗ et d'un mot infini v ∈ Σ^ω sera simplement notée uv, et définie précisément comme la suite (a_i)_(i ∈ ℕ) telle que a_i = u_i pour i < |u| et a_(j + |u|) = v_j pour j ∈ ℕ. On écrira généralement un mot infini w ∈ Σ^ω comme w_0 w_1…
On dit que u ∈ Σ^∗ est un préfixe du mot v (fini ou infini) quand v s'écrit uw pour un certain mot w. Le préfixe u est dit strict quand u ≠ v.
Pour un mot fini u ∈ Σ^∗ de longueur n ∈ ℕ et une lettre a ∈ Σ, on note
|u|_a = ^(def)|{i < n|u_i = a}|
le nombre de positions dans u où la lettre a apparaît. On étend naturellement cette notion aux mots infinis, et l'on aura |u|_a = + ∞ si le mot contient la lettre a à une infinité de positions.
Dans ce sujet, quand on parle d'un mot sans plus de précision, il peut être fini ou infini. À l'inverse, on précisera toujours quand un mot est supposé fini (resp. infini).
Langage C
Dans ce sujet, on utilisera le mot anglais thread pour parler de fil d'exécution dans le cadre de la programmation concurrente, aussi appelée programmation multi-threads.
On considèrera toujours que tous les en-têtes nécessaires à un programme C sont implicitement inclus. Cela concerne notamment les en-têtes usuels tels que <assert.h>, <stdio.h> ou <stdlib.h>, mais aussi l'en-tête <pthread.h>. Cette convention vaut pour les programmes présentés dans le sujet, mais s'applique aussi aux programmes que le candidat devra écrire : il n'est pas demandé d'inclure explicitement les en-têtes.
On peut définir et initialiser un tableau statique de n entiers grâce à l'instruction int tableau [n] = {e_0, e_1, …, e_(n − 1)}; où n est une constante littérale entière et les e_i pour i ∈ {0, …, n − 1} sont des expressions de type int.
Langage OCaml
Dans le reste du sujet, on pourra utiliser les fonctions suivantes de la bibliothèque standard OCaml pour les listes. Les hypothèses de complexité données, légèrement simplifiées par rapport à la réalité, pourront être utilisées pour des listes sur des types pour lesquels l'égalité est en temps constant - on supposera que c'est le cas pour les états, actions, et paires de ces objets, manipulés dans le sujet.
-Si lst1 et lst2 sont deux listes, lst1 @ lst2 est leur concaténation. Elle se calcule en temps linéaire en la longueur de lst1. L'ajout e::lst d'un élément e en tête de la liste lst se fait en temps constant.
-List.mem x lst renvoie true s'il existe un élément e de la liste lst tel que x = e, et false sinon. Elle s'exécute en temps linéaire en la longueur de lst.
-List.assoc k [(k_1, v_1); …; (k_n, v_n)] renvoie le premier v_i tel que k = k_i s'il existe, et lève l'exception Not_found sinon. Elle s'exécute en temps linéaire en la longueur de la liste.
-List.iter f [e_1; …; e_n] exécute f e_i pour i = 1, 2, …, n, et renvoie (). Son temps d'exécution est la somme des temps d'exécution des f e_i.
-List.map f [e_1; …; e_n] renvoie [f e_1; …; fe_n ]. Son temps d'exécution est la somme des temps d'exécution des f e_i.
-List.filter f lst renvoie la liste des éléments e de lst tels que f e = true, dans l'ordre. Son temps d'exécution est la somme des temps d'exécution des f e.
-List.fold_left f a_0[e_1; …; e_n] renvoie a_n où a_(k + 1) = fa_k e_(k + 1) pour tout 0 ≤ k < n. Son temps d'exécution est la somme des temps d'exécution des f a_k e_(k + 1).
Pour les tableaux, on pourra utiliser les fonctions suivantes :
-Array.init n f renvoie le tableau [|f 0;…;f (n-1)|] pour n positif. Son temps d'exécution est la somme des temps des f i pour i = 0, 1, . . , , n-1.
-Array.make n v renvoie le tableau [|v;...;v|] de taille n dont chaque case contient la même valeur v, pour n positif. Son temps d'exécution est linéaire en n.
Partie I : Systèmes de transitions
Un système de transitions T = (S, A, D, δ) est composé de :
-un ensemble fini d'états S;
-un ensemble fini d'actions A;
-une application D : S → 2^A indiquant pour chaque état s l'ensemble D(s) des actions disponibles en s;
-une fonction partielle δ : S × A → S de domaine {(s, α) ∈ S × A|α ∈ D(s)} indiquant pour chaque état s et α ∈ D(s) le résultat δ(s, α) de la transition α depuis s.
On écrira s → ^α s^′ pour δ(s, α) = s^′. Pour u = u_0…u_n ∈ A^∗, on écrira s → ^u s^′ pour signifier qu'il existe des états s_i tels que s→−^(u_0)s_0→−^(u_1)⋯→−^(u_n)s^′. Un état s est dit terminal si D(s) = ∅.
Figure 1 - Représentation graphique d'un système de transitions simple.
On pourra représenter graphiquement un système de transitions comme on le fait pour les automates finis. Ainsi, la figure 1 représente le système T = (S, A, D, δ) avec S = {s_1, s_2, s_3, s_4} et A = {α, β, γ}, où l'on a
et δ est donnée par les arcs entre états, avec notamment δ(s_1, α) = s_2 et δ(s_3, γ) = s_3. Dans ce système, s_4 est le seul état terminal, i.e. le seul état s tel que D(s) = ∅.
Une exécution infinie dans T est donnée par un état de départ s_0 ∈ S et une suite d'actions (α_i)_(i ∈ ℕ) telles que l'état successeur s_(i + 1) = δ(s_i, α_i) est défini pour tout i ∈ ℕ. Quand π = (s_0, (α_i)_(i ∈ ℕ)) définit une exécution infinie, on notera simplement :
La trace d'une telle exécution infinie est la suite des états de l'exécution :
trace(π) = (s_i)_(i ∈ ℕ)
Sur l'exemple de la figure 1, on a des exécutions infinies à partir de s_1 avec pour trace s_1 s_2 s_1 s_2… mais aussi s_1 s_2 s_1 s_2 s_3 s_3… Il n'y a qu'une exécution infinie à partir de s_3, dont la trace est s_3 s_3 s_3…
On définit de façon analogue les exécutions finies, données par un état de départ s_0 ∈ S, une longueur n ∈ ℕ, et une suite d'actions (α_i)_(i < n) telle que s_(i + 1) = δ(s_i, α_i) est défini pour tout i < n. Pour indiquer que π = (s_0, (α_i)_(i < n)) est une exécution finie, on notera :
π : s_0→−^(α_0)s_1→−^(α_1)…→−^(α_(n − 1))s_n
Dans ce cas-là, on dira qu'il existe une exécution de s_0 à s_n, ou que s_n est accessible à partir de s_0. Plus généralement, on dira qu'un ensemble d'états E est accessible à partir de s_0 s'il existe un état s ∈ E accessible à partir de s_0.
Attention ! Un système de transitions sur un ensemble d'actions A ressemble beaucoup à un automate fini sur l'alphabet A, mais il ne faut pas confondre les deux notions. On remarquera notamment qu'un système de transitions n'a pas d'état initial distingué : n'importe quel état du système peut servir de point de départ pour des exécutions. De plus, la notion d'état terminal dans un système de transitions ne doit pas être confondue avec la notion d'état final dans un automate : elle ne témoigne pas de l'acceptation d'un mot, mais simplement de l'impossibilité d'effectuer toute transition.
Dans ce sujet, on utilise à la fois les systèmes de transitions et les automates finis, mais dans des rôles différents. Pour bien les distinguer, on réserve l'usage de la lettre s pour les états des systèmes de transitions, et la lettre q pour les états des automates.
Une propriété de trace pour un système de transitions T = (S, A, D, δ) est un ensemble P ⊆ S^ω de mots infinis sur S. On dit qu'un état s satisfait la propriété P quand, pour toute exécution infinie π à partir de s, on a trace(π) ∈ P. On écrit alors s⊨P.
On remarquera qu'un état s à partir duquel aucune exécution infinie n'est possible satisfait trivialement toute propriété P. Sous cette condition, on a même s⊨P et s⊨S^ω∖P, ce qui ne peut arriver dès lors que s admet au moins une exécution infinie.
On illustre la notion de satisfaction sur l'exemple de la figure 1 avec P l'ensemble des mots w ∈ S^ω tels que w_i = s_2 pour au moins une position i ∈ ℕ. On a alors s_1⊨P et s_2⊨P car toutes les exécutions infinies à partir de ces états ont s_2 dans leur trace, mais s_3⊭P. On a trivialement s_4⊨P car il n'existe pas d'exécution infinie à partir de s_4.
Un invariant de T est un ensemble d'états I ⊆ S tel que, pour tous s ∈ I et α ∈ D(s), on a δ(s, α) ∈ I. On pourra remarquer que l'ensemble vide et l'ensemble de tous les états sont des invariants triviaux, pour tout T. Sur l'exemple de la figure 1, les seuls invariants non-triviaux sont {s_3, s_4} et {s_4}.
Question 1. Soit I ⊆ S. On considère la propriété P = I^ω, i.e. l'ensemble des mots infinis sur I. Montrer que les conditions suivantes sont équivalentes quand T ne contient aucun état terminal :
(a)L'ensemble I est un invariant.
(b)Pour tout s ∈ I, on a s⊨P.
Une propriété P est une propriété de sûreté quand, pour tout mot w ∈ S^ω∖P, il existe un préfixe w^ ∈ Σ^∗ de w tel que pour tout w^′ ∈ S^ω on a encore w^w^′ ∉ P.
Par exemple, si l'on fixe s ∈ S, alors P_1 = {w ∈ S^ω||w|_s = 0} est une propriété de sûreté : tout w ∉ P_1 contient s à une certaine position i, et si l'on prend pour w^ le préfixe de w de longueur i + 1, on a bien w^w^′ ∉ P_1 pour tout w^′ ∈ S^ω. En revanche, P_2 = {w ∈ S^ω||w|_s = + ∞} n'est pas une propriété de sûreté dès lors qu'il existe un état s^′ ≠ s : en effet, le mot w = s^′ s^′ s^′… n'est pas dans P_2, mais pour tout préfixe w^ de w, on a w^s ss_… ∈ P_2.
Question 2. On suppose dans cette question que S contient au moins deux états distincts. Parmi les propriétés suivantes, lesquelles sont des propriétés de sûreté?
1.P_1 = {w ∈ S^ω||w|_s = 0 pour au moins un s ∈ S}
2.P_2 = {w ∈ S^ω||w|_s = + ∞ pour tout s ∈ S}
3.P_3 = {w ∈ S^ω||w|_s < + ∞ pour au moins un s ∈ S}
Soit P une propriété de sûreté. On dit qu'un mot fini u ∈ S^∗ est un préfixe interdit pour P quand, pour tout v ∈ S^ω, on a uv ∉ P. Le préfixe interdit u est minimal si aucun de ses préfixes stricts n'est lui-même préfixe interdit. On définit P^ comme l'ensemble des préfixes interdits minimaux pour P. On dit que la propriété P est régulière quand P^ est un langage régulier, au sens usuel des langages réguliers de mots finis.
Question 3. On suppose pour cette question que le système T n'a que deux états, et l'on pose S = {a, b}. Parmi les propriétés de sûreté suivantes, lesquelles sont régulières? Justifier vos réponses, en donnant notamment un automate reconnaissant P^_i quand P_i est régulière.
1.P_1 = {w ∈ S^ω||u|_a = 0 pour tout u ∈ Σ^∗ préfixe de w}
2.P_2 = {w ∈ S^ω|‖u|_a − |u|_b|≤1 pour tout u ∈ Σ^∗ préfixe de w}
3.P_3 = {w ∈ S^ω||u|_a ≤ |u|_b pour tout u ∈ Σ^∗ préfixe de w}
4.P_4 = {w ∈ S^ω||u|_a ≤ |u|_b ≤ |u|_a + 1 pour tout u ∈ Σ^∗ préfixe de w}
Question 4. Soit P une propriété de sûreté. Montrer que l'ensemble de ses préfixes interdits minimaux est régulier si et seulement si l'ensemble de ses préfixes interdits est régulier.
Afin de vérifier algorithmiquement des propriétés de systèmes de transitions, on introduit quelques structures de données en OCaml. Par simplicité, on représentera les états des systèmes de transitions par des entiers dans un intervalle fini : on pose type state = int. On suppose disposer d'un type action pour représenter les actions ; il est inutile de le préciser.
Un système de transitions sera décrit par un enregistrement du type suivant :
type trans_sys = {
size : int; (* états = { 0,1,...,size-1 } *)
next : state -> (action * state) list; (* transitions *)
}
Plus précisément, une valeur t de ce type représente le système de transitions dont :
-les états sont les entiers positifs (ou nuls) strictement inférieurs à t.size;
-les transitions à partir d'un état s sont données par t.next s, qui est une liste d'associations sans répétition d'une même action : on a s a s' pour tout (a, s') dans t.next s.
Les actions disponibles D(s) sont données implicitement via t.next s. On supposera que la fonction t.next s'exécute en temps constant.
Question 5. Étant donné un système de transitions T, un état s_0 et un ensemble d'états cible C, on souhaite vérifier algorithmiquement l'accessibilité de C à partir de s_0. Implémenter en OCaml une fonction
val reachable : trans_sys -> state -> (state -> bool) -> bool
telle que reachable t s0 target = true quand il existe un état s de t accessible à partir de s0 et tel que target s = true. Votre fonction devra terminer sur toute entrée, en supposant seulement que s0 est un état valide de t. Justifier brièvement sa correction, et évaluer sa complexité en temps en fonction des nombres d'états et d'actions de t.
On démontrerait aisément que les mots d'une propriété de sûreté P sont exactement ceux dont aucun préfixe n'est dans P^, et l'on admet ce résultat dans la suite :
éP = {w ∈ S^ω|u ∉ P^ pour tout préfixe u de w}
Question 6. Soit T = (S, A, D, δ) un système de transitions sans état terminal, et P ⊆ S^ω une propriété de sûreté dont les préfixes interdits minimaux sont reconnus par un automate fini A déterministe et complet. On souhaite ramener le problème de la satisfaction de P à un problème d'inaccessibilité dans un système de transitions construit à partir de T et A. Pour cela, construire un système de transitions T_A =(S^′, A, D^′, δ^′), une application ι : S → S^′ et un sous-ensemble C ⊆ S^′ tels que, pour tout s ∈ S, on a s⊭P si et seulement si C est accessible à partir de ι(s) dans T_A. On justifiera cette équivalence.
L'objectif de cette question est de donner une construction qui mènerait immédiatemment à un algorithme pour la satisfaction, mais on ne demande ici qu'une description mathématique de cette construction.
Partie II : Ultime invariance
Soit T = (S, A, D, δ) un système de transitions, et Φ ⊆ S. On définit la propriété de trace EAΦ comme suit :
EA Φ = ^(def){w ∈ S^ω|∃i ∈ ℕ, ∀j ≥ i, w_j ∈ Φ}
La notation EA Φ vient de l'anglais eventually always Φ, qu'on peut traduire littéralement comme au bout d'un moment, on est tout le temps dans Φ. De façon plus concise, on appellera EAΦ la propriété d'ultime invariance de Φ.
Figure 2 - Exemple de système sur A = {α, β}.
Question 7. On suppose pour cette question seulement que T est le système décrit en figure 2. On pose Φ = {s_2, s_4, s_5, s_6}. Pour quels états s a-t-on s⊨Φ^ω ? Même question pour s⊨EAΦ.
Question 8. On considère désormais un système T quelconque. Soit s un état de T. Montrer que s⊭ EA Φ si et seulement s'il existe un état s^′ accessible à partir de s tel que s^′ ∉ Φ et s^′ fait un cycle sur lui-même, i.e. il existe une exécution de longueur non-nulle de s^′ à s^′.
Vérifier s_0⊨EAΦ se ramène donc à vérifier qu'il n'existe pas d'état accessible à partir de s_0 qui soit hors de Φ et dans un cycle. Cela peut se faire par plusieurs parcours du système de transitions. On peut d'abord lancer un parcours (appelé externe) à partir de s_0 pour déterminer tous les états s^′ accessibles à partir de s_0. Lors de ce parcours externe, pour chaque état s^′ visité tel que s^′ ∉ Φ, on lance un nouveau parcours (appelé interne) à partir de s^′ pour déterminer s'il est dans un cycle.
Cette approche naïve peut cependant être améliorée en évitant des parcours internes redondants : c'est l'algorithme des parcours imbriqués donné en figure 3. Ici, le parcours externe a vocation à être encapsulé dans la fonction outer_iter, qui doit itérer sur les états accessibles à partir de s_0 - sa spécification précise et son implémentation sont l'objet des prochaines questions. Les parcours internes sont réalisés par la fonction cycle_check. Il faut bien noter que les multiples parcours internes partagent le tableau marked : ainsi, un état ne sera traité par inner_visit qu'au plus une fois lors de tous les appels à cycle_check au sein d'un même appel à verify.
(** Parcours externe: [outer_iter t sO f] appelle la fonction [f] sur
tous les états [s] accessibles à partir de [s0]. *)
let outer_iter (t:trans_sys) (s0:state) (f:state->unit) : unit =
... (* Implémentation à réaliser en question 10. *)
(** Exception utilisée pour interrompre la recherche. *)
exception Found
(** [verify t phi sO] vérifie que [sO] satisfait [EA phi].
Ici [phi] n'est pas un ensemble mais une fonction,
représentant l'ensemble des états [s] tels que [phi s = true]. *)
let verify (t:trans_sys) (phi:state->bool) (s0:state) : bool =
(* Parcours internes: détection de cycle *)
let marked = Array.make t.size false in
let cycle_check start =
let rec inner_visit s =
if not marked.(s) then begin
marked.(s) <- true;
List.iter
(fun (_,s') ->
if s' = start then raise Found;
inner_visit s')
(t.next s)
end
in
inner_visit start
in
(* Parcours externe: états accessibles *)
try
outer_iter t s0
(fun s' -> if not (phi s') then cycle_check s');
true
with Found -> false
Figure 3 - Algorithme des parcours imbriqués.
Question 9. Soit s_0 un état quelconque de T. Un parcours en profondeur à partir de s_0 induit un arbre couvrant l'ensemble des états accessibles dans T à partir de s_0. On considère une énumération des états aux nœuds de cet arbre satisfaisant les deux conditions suivantes :
(a)L'énumération suit un ordre postfixe.
(b)Si deux nœuds s et s^′ ont le même parent dans l'arbre, et que s a été visité avant s^′ dans le parcours en profondeur, alors s (et tous ses descendants dans l'arbre) apparaît avant s^′ (et tous ses descendants) dans l'énumération.
Montrer que, si un état s_1 apparaît avant s_2 dans cette énumération, et qu'il existe une exécution de s_1 à s_2 dans T, alors s_1 fait un cycle sur lui-même dans T, i.e. il existe une exécution de longueur non-nulle de s_1 à s_1 dans T.
Question 10. Donner une implémentation de outer_iter telle que outer_iter t s f itère la fonction f sur tous les états accessibles à partir de s dans t, selon un ordre satisfaisant les conditions (a) et (b) de la question précédente.
Question 11. On suppose que outer_iter implémente correctement la spécification donnée dans la question précédente. Proposer une spécification pour la fonction cycle_check, montrer la correction de la fonction par rapport à cette spécification, et s'appuyer sur ce résultat pour démontrer la correction de verify. On ne demande pas de démontrer la terminaison. On attend en revanche des pré- et postconditions sur l'état du tableau marked ainsi qu'une spécification des cas où l'exception Found est levée.
Question 12. La fonction verify reste-t-elle correcte si l'on modifie outer_iter pour parcourir les états accessibles selon un ordre préfixe au lieu de postfixe? Justifier votre réponse, en expliquant comment l'argument de la question précédente s'adapte, ou en donnant un contre-exemple à moins de quatre états.
Partie III : Programmation multi-threads en C
On rappelle les éléments de programmation concurrente en C au programme, via trois fonctions déclarées dans l'en-tête <pthread.h> et dont les prototypes sont donnés en figure 4 :
-La fonction pthread_create lance un nouveau thread (en français, fil d'exécution). Elle prend en premier argument un pointeur pthread vers un descripteur de thread dans lequel les informations concernant le nouveau thread seront remplies, pour permettre de s'y référer ensuite. En second argument, elle prend un pointeur attr vers des attributs de threads; il n'est pas utile d'en connaître la teneur exacte, car on se contentera de donner ici une valeur par défaut décrite plus loin. En troisième argument, elle prend une fonction start_routine qui attend un argument de type void* et renvoie une valeur du même type - on ignorera cette valeur de retour dans le sujet. En dernier argument est donnée la valeur arg qui sera passée à la fonction start_routine pour démarrer le calcul dans le nouveau thread.
-La fonction pthread_join permet d'attendre la fin de l'exécution du thread dont le descripteur est donné en premier argument. Dans ce sujet, on lui passera toujours NULL en second argument, et on ignorera l'entier renvoyé.
-La fonction pthread_attr_init prend en argument un pointeur vers des attributs de thread, et initialise ces attributs avec des valeurs par défaut. On ignorera l'entier renvoyé.
On donne en figure 5 un exemple de programme illustrant l'usage de ces fonctions. Cet extrait de programme suppose la donnée préalable d'une fonction max calculant le maximum de deux valeurs de type int. On y exploite par ailleurs la représentation des tableaux en C : un tableau est un pointeur, dont la valeur est l'adresse du premier élément du tableau. Plus généralement, l'adresse de l'élément d'index k d'un tableau de n éléments représente le tableau de n − k éléments commençant à l'index k. Ainsi, en figure 5, l'expression &(array[2]) représente la deuxième moitié du tableau array et, quand le deuxième thread accède à ((int*)pair) [0], il lit en fait la valeur array[2].
Question 13.
a.Quels sont les affichages possibles à la fin de l'exécution du programme de la figure 5 ? Expliquer brièvement comment chaque valeur peut être obtenue.
b.L'objectif de ce programme est manifestement d'obtenir dans m le maximum des quatre entiers contenus dans array. Quelle est la façon canonique de corriger notre programme pour éviter les exécutions menant à un résultat incorrect? Proposer une correction du programme par l'insertion de quelques lignes ^1. On ne demande pas de produire du code C valide : une description haut niveau des instructions à insérer suffit.
/* Les types pthread_t (descripteur de thread)
et pthread_attr_t (attributs de thread)
sont déclarés dans <pthread.h>.
Il est inutile de préciser leurs définitions. */
int pthread_attr_init(pthread_attr_t *attr);
int pthread_create(pthread_t *pthread,
pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
int pthread_join(pthread_t pthread, void **retval);
Figure 4 - Fonctions de la bibliothèque pthread.
int m = -1;
void* max2(void* pair) {
int m1 = ((int*)pair)[0];
int m2 = max(m1, ((int*)pair)[1]);
int m3 = max(m2, m);
m = m3;
return NULL;
}
int main() {
int array[4] = {10, 20, 30, 40};
/* Attributs par défaut des threads */
pthread_attr_t attr;
pthread_attr_init(&attr);
/* Création des threads */
pthread_t thread1, thread2;
pthread_create(&thread1, &attr, max2, array);
pthread_create(&thread2, &attr, max2, &(array[2]));
/* Attente de fin et affichage du résultat */
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("m == %d\n", m);
}
Figure 5 - Exemple de programme C multi-threads.
On souhaite généraliser le programme de la figure 5 pour calculer le maximum d'un tableau en répartissant le travail entre plusieurs threads. On suppose déclarées trois variables globales :
-const int nb_threads indiquant le nombre de threads à utiliser ;
-const int items_per_thread indiquant combien de valeurs chaque thread devra traiter;
-int m, initialisée à -1, destinée à contenir le résultat final du calcul comme dans l'exemple de la figure 5.
On suppose enfin qu'on dispose d'une fonction void* local_max(void* a) analogue à max2 mais pour un tableau de items_per_thread éléments plutôt que deux. Plus précisément :
-La fonction doit être appelée (précondition) avec en argument a l'adresse d'un tableau contenant (au moins) items_per_thread éléments de type int, tous positifs.
-À titre indicatif, la fonction calcule alors le maximum v de ces items_per_thread éléments, puis effectue l'affectation m = v si la variable globale m est inférieure à v. Le fonctionnement précis de la fonction n'a cependant pas d'importance dans la suite du sujet.
Question 14. Écrire une fonction main qui :
a.Alloue sur le tas un tableau array de nb_threads*items_per_thread valeurs de type int.
b.Initialise chaque case du tableau avec un entier positif pseudo-aléatoire. Celui-ci sera simplement obtenu via l'expression rand()%max_item qui renvoie un entier entre 0 (inclus) et max_item (exclu), cette dernière variable étant supposée déclarée globalement.
c.Lance nb_threads exécutant tous la fonction local_max, appelée dans chaque thread sur l'adresse d'un sous-tableau de array commençant à un index de la forme k*items_per_thread, de sorte que chaque valeur du tableau sera prise en compte par exactement un thread.
d.Affiche la valeur de m une fois que tous les threads ont terminé leur exécution.
Cette fonction ne devra occasionner aucune fuite de mémoire. On ne demande pas dans cette question de régler d'éventuels problèmes d'accès concurrents comme en question 13.
Partie IV : Systèmes de transitions concurrents
Les systèmes de transitions sont souvent utilisés pour modéliser des systèmes concurrents, par exemple des programmes multi-threads C. Dans ce cas, la notion d'indépendance entre actions concurrentes induit un quotient sur l'espace des traces, appelant à la vision d'une trace comme un ordre partiel. Ces notions peuvent être exploitées pour réduire le nombre d'états et/ou de traces à explorer lors de la vérification d'un système. On introduit certaines de ces techniques dans cette partie, en s'appuyant sur des exemples correspondant à des programmes multi-threads C pour les illustrer.
On s'intéresse tout d'abord à la modélisation de programmes multi-threads par des systèmes de transitions, que nous illustrons sur l'exemple de la figure 5, dans sa version originale sans prendre en compte la correction demandée en question 13. Ce programme comporte deux threads dont chacun peut effecter quatre instructions, aux lignes 4 à 7 ; on ignorera ici l'instruction return. On le modélise comme un système de transitions dont les états sont les états du programme, qui sont donnés par la valeur des variables locales et globales, et la position de chaque thread dans son exécution. Ces positions seront prises dans {0, 1, 2, 3, 4}, la position j indiquant que le thread a exécuté ses j premières instructions. On utilisera des actions de la forme t_i^j avec i ∈ {1, 2} et j ∈ {1, 2, 3, 4} : l'action t_i^j représente l'exécution de la èj^(ème) instruction du thread i.
On définit quelques états, en utilisant une notation expliquée juste après :
L'état s_0 correspond à l'état initial de notre programme : la variable globale m vaut -1 ; chaque thread i est à la position 0 , ce que l'on note t_i@0; aucune variable locale n'est spécifiée car aucune n'est encore définie. Dans cet état, deux actions sont disponibles : t_1^1 et t_2^1. La transition t_2^1 mène à l'état s^′, où le second thread est passé à la position 1 et a défini sa variable locale m 1 avec pour valeur 30 . Ensuite, l'état s_1 correspond à la situation où chaque thread a défini ses deux premières variables locales et s'apprête à définir la dernière, en fonction de la valeur de la variable globale m. Enfin, on a s^(′′) = δ(s_1, t_1^3).
On notera que cette modélisation considère chaque instruction du programme comme étant atomique. C'est une hypothèse raisonnable sur notre exemple, où une instruction ne contient pas plus d'une lecture ou écriture sur une variable globale.
Question 15. À partir de l'état s_0, notre système de transitions permet des exécutions de longueur au plus huit. Combien d'exécutions différentes de longueur huit sont-elles possibles?
Attention ! Dans la fin du sujet, les systèmes modélisant des programmes C sont utilisés uniquement pour illustrer des notions générales liées à la concurrence dans les systèmes de transitions. On prendra garde à ne pas interpréter ces notions dans le contexte restrictif des programmes C, et à traiter en toute généralité les questions qui ne font pas explicitement référence à un exemple.
Dans un système de transitions T = (S, A, D, δ), on dit que deux actions α, β ∈ A sont indépendantes quand α ≠ β et les conditions suivantes sont satisfaites :
-Exécuter α ne change pas la disponibilité de β : pour tout s ∈ S tel que α ∈ D(s), on a β ∈ D(s) ⇔ β ∈ D(δ(s, α)).
-Exécuter β ne change pas la disponibilité de α : pour tout s ∈ S tel que β ∈ D(s), on a α ∈ D(s) ⇔ α ∈ D(δ(s, β)).
-L'ordre d'exécution de α et β n'a pas d'importance : pour tout s ∈ S tel que α ∈ D(s) et β ∈ D(s), on a δ(δ(s, α), β) = δ(δ(s, β), α). L'indépendance de α et β est notée α ↔ β. Elle peut être résumée de façon moins précise mais visuelle comme suit, pour tout état s, avec s^′ = δ(δ(s, α), β) = δ(δ(s, β), α) :
Pour un mot u ∈ A^∗, on écrira α ↔ u quand α ↔ u_i pour tout i < |u|. Pour un ensemble d'actions E ⊆ A, on écrira α ↔ E quand α ↔ β pour tout β ∈ E.
Question 16. Dans le système correspondant à la figure 5, a-t-on indépendance des deux actions disponibles en s_0 ? Même question pour les états s^′, s_1 et s^(′′).
Pour s_0 ∈ S un état, on dit qu'un ensemble X ⊆ D(s_0) est un ensemble persistant pour s_0 quand les deux conditions suivantes sont satisfaites :
(A1)X ≠ ∅ si D(s_0) ≠ ∅.
(A2)Pour toute exécution s_0→−^(α_0)…→−^(α_n)s_(n + 1) à partir de s_0 et telle que α_i ∉ X pour tout i ≤ n, on a α_i ↔ X pour tout i ≤ n.
La seconde condition impose que, si l'on exécute à partir de s_0 une séquence d'actions qui ne sont pas dans X, ces actions seront nécessairement indépendantes de toutes les actions de X. On notera que D(s_0) est trivialement persistant pour s_0.
Question 17. Soit s_0 un état, X un ensemble persistant pour s_0, et α ∈ X. On considère une exécution comme dans la condition (A2), i.e. de la forme s_0→−^(α_0)…→−^(α_n)s_(n + 1) avec α_i ∉ X pour tout i ≤ n. Montrer que α ∈ D(s_i) pour tout i ≤ n + 1.
Question 18. Sur notre exemple issu de la figure 5, donner un ensemble persistant minimal en s_0, i.e. un ensemble persistant dont aucun sous-ensemble strict n'est persistant. Même question en s_1.
La question suivante énonce la propriété fondamentale des ensembles persistants : les états terminaux accessibles le restent si l'on se restreint aux exécutions dont toutes les transitions sont choisies dans des ensembles persistants.
Question 19. On se donne une application X : S → 2^A telle que, pour tout s ∈ S, l'ensemble d'actions X(s) est un ensemble persistant pour s. Soit s_0 un état quelconque, et s_t un état terminal accessible à partir de s_0 dans T : on a donc une exécution de s_0 à s_t, mais aucune exécution possible à partir de s_t. Montrer qu'il existe une exécution
de longueur n ≥ 0 telle que α_i ∈ X(s_i) pour tout i < n.
Pour un système de transitions donné T, on définit la relation ~ d'équivalence par permutation sur les mots de A^∗ comme la plus petite relation d'équivalence sur A^∗ telle que, pour toutes les actions α, β ∈ A satisfaisant α ↔ β, on a uαβv ∼ uβαv pour tous u, v ∈ A^∗. On remarque immédiatemment que si u, v ∈ A^∗ sont tels que u ∼ v, alors pour tous s, s^′ ∈ S tels que s → ^u s^′, on a aussi s → ^v s^′.
On définit A^≠comme l'ensemble des mots sans répétition sur A, i.e. l'ensemble des mots u ∈ A^∗ tels que u_i ≠ u_j pour tous 0 ≤ i < j < |u|. Travailler avec A^≠plutôt que A^∗ simplifie la suite du sujet.
Étant donné un mot u ∈ A^≠de longueur n ∈ ℕ, on définit l'ordre induit < _u sur l'ensemble {u_0, …, u_(n − 1)} des lettres de u comme la plus petite relation transitive satisfaisant la condition suivante : pour tous i, j ∈ {0, …, n − 1} tels que i < j et u_i⇍u_j, on a u_i < _u u_j.
Question 20. On illustre la notion d'ordre induit sur quelques exemples simples.
a.On suppose que T est construit sur A = {α, β, γ} de sorte que β↪̸α et βb̸γ mais α ↔ γ. Donner les ordres induits pour u = αβγ, pour v = αγβ et pour w = γαβ.
b.On suppose que T est construit sur A = {α, α^′, β, β^′} de sorte que α↛β et α^′↪̸β^′, mais on a indépendance pour toutes les autres paires d'actions distinctes. Donner les ordres induits pour u = αα^′ ββ^′, pour v = αβα^′ β^′ et pour w = βαα^′ β^′.
Question 21. Soient u, v ∈ A^≠de même longueur n ∈ ℕ, et contenant les mêmes lettres, i.e. {u_0, …, u_(n − 1)} = {v_0, …, v_(n − 1)}. Montrer qu'on a u ∼ v si et seulement si < _u et < _v coïncident.
On définit enfin les exécutions avec mise en veille. Étant donnés un système de transitions T = (S, A, D, δ) et un ordre total < sur A, le système T^< = (S^<, A, D^<, δ^<) est défini comme suit :
-S^< = S × 2^A, i.e. les états de S^<sont constitués d'un état de S auquel on adjoint un ensemble d'actions ;
-pour tout état (s, Z) ∈ S^<, on a D^<((s, Z)) = D(s)∖Z;
-pour tous (s, Z) ∈ S^<et α ∈ D^<(s), on a δ^<((s, Z), α) = (δ(s, α), Z^′) avec Z^′ = {β ∈ Z|α ↔ β} ∪ {β ∈ D(s)|α ↔ β, β < α}.
Figure 6 - Deux threads pour illustrer la mise en veille.
Question 22. On considère un programme comportant deux threads exécutant respectivement les deux fonctions de la figure 6, manipulant une variable globale v de type int initialisée à 0. Soit T le système de transitions modélisant ce programme, dans le même style que précédemment, sur A = {t_1^1, t_1^2, t_2^1, t_2^2} correspondant aux instructions des lignes 2 et 3 dans thread1 et thread2 respectivement. Soit s_0 l'état correspondant à l'état initial du programme.
a.Dessiner le système de transitions T, en indiquant l'action associée à chaque transition et la valeur des variables locales et globale définies en chaque état. Il est inutile de décrire complètement les états comme on l'a fait en début de partie pour certains états du programme de la figure 5.
b.On considère l'ordre sur les actions donné par t_1^1 < t_1^2 < t_2^1 < t_2^2. Dessiner les états accessibles à partir de (s_0, ∅) dans T^<et les transitions entre ceux-ci. On pourra se contenter d'indiquer (idéalement, en couleur) les modifications sur le dessin précédent : quels ensembles Z sont associés à chaque état, et quelles transitions ne sont plus possibles dans T^<.
On munit A^≠de l'ordre lexicographique induit par l'ordre < sur les actions. L'ordre lexicographique sera simplement noté <. Cela permet de définir, pour tout mot u ∈ A^≠, u^<comme le minimum lexicographique de la classe d'équivalence de u pour ∼. (Ce minimum est bien défini car la classe d'équivalence ne contient que des mots de même longueur que u.)
Question 23. Soit u ∈ A^≠. Montrer que u = u^<si et seulement si u ne peut pas s'écrire u_1 βu_2 αu_3 avec u_1, u_2, u_3 ∈ A^≠et α, β ∈ A tels que α < β et α ↔ βu_2.
Question 24. En déduire que la mise en veille sélectionne les minima lexicographiques. Plus précisement, démontrer les deux résultats suivants pour tous u ∈ A^≠et s, s^′ ∈ S :
a.Pour tous Z, Z^′ ⊆ A, si on a (s, Z) → ^u(s^′, Z^′) dans T^<, alors u = u^<.
b.Si s → ^u s^′ dans T, alors il existe Z tel que (s, ∅)→−^(u^<)(s^′, Z) dans T^<.
Fin du sujet.
Bien sûr, la correction devra fonctionner quelles que soient les quatre valeurs initialement contenues dans array, dès lors qu'elles sont positives.
Questions fréquentes
4 questions
Sur quels chapitres porte l'épreuve d'informatique C de la filière MPI (X-ENS) 2026 ?
Afficher ou masquer la section
Sur quels chapitres porte l'épreuve d'informatique C de la filière MPI (X-ENS) 2026 ?
+
Elle porte sur les systèmes de transitions et la vérification de propriétés (sûreté, ultime invariance), la programmation en OCaml, la programmation concurrente en langage C avec la bibliothèque pthread, et les techniques de réduction d'ordre partiel pour la vérification de systèmes concurrents.
Les parties du sujet sont-elles indépendantes ?
+
Les définitions de la partie I sont utiles dans tout le sujet. La partie II est indépendante du reste. La partie III fournit des rappels et un exemple de programme réutilisé en partie IV, qui s'appuie donc sur les parties précédentes.
Quels résultats de cours faut-il connaître pour traiter ce sujet ?
+
Les automates finis et les langages réguliers, la programmation fonctionnelle en OCaml et l'analyse de complexité, ainsi que les bases de la programmation concurrente en C (création et synchronisation de threads, accès concurrents aux données partagées).
Ce sujet demande-t-il d'écrire du code ?
+
Oui, il demande d'écrire plusieurs fonctions en OCaml (accessibilité, parcours en profondeur) dans les parties I et II, ainsi que des programmes ou des corrections de programmes en langage C utilisant pthread dans la partie III.
Pas de description pour le moment
Commentaires• X ENS Informatique C MPI 2026
Connectez-vous pour participer aux discussions
Partagez vos avis, posez des questions et échangez avec la communauté