Retrouvez cet article dans : Linux Magazine 86
1. Les processus
1.1 Présentation
Un processus est une exécution d’un programme à un moment donné. Au niveau du système, un processus est caractérisé par un PCB (Process Control Block). Sous Unix, le PCB s’appelle CORE (image). Le PCB contient :- le contexte mémoire ;
- les segments mémoire :
- le segment de code : contient le code du programme exécuté par le processus. Ce segment peut être partagé par plusieurs processus.
- le segment de données : c’est la zone des variables globales et statiques, ainsi que du tas, géré dynamiquement. Ce segment est privé au processus.
- le segment de pile : c’est la zone de transmission des paramètres, des retours de fonctions et des variables locales. Ce segment est privé au processus.
- l’environnement : ensemble des variables et de leurs valeurs (
TERM=vt100, par exemple). Cet environnement est accessible à un programme C via la variable environ (voirenviron(7)). Les fonctions de la bibliothèque standard,getenv(3)etputenv(3)permettent, respectivement, d’obtenir la valeur d’une variable d’environnement et de la positionner. En Ruby, l’environnement d’un processus est stocké dans la " constante " globaleENV(qui se comporte comme un hachage dont les clés seraient les noms des variables) :
ENV.each { |cle, val| puts("#{cle} : #{val}")}
puts(ENV[‘PATH’])
ENV[‘PATH’].split(":").each { |rep| puts(rep) }
ENV[‘MA_VAR’] = ‘Ok’
- La priorité du processus pour l’attribution de l’UC.
- Les descripteurs des fichiers utilisés (le nombre maximum dépend du système). Il y en a toujours au moins 3, qui sont ouverts automatiquement à la création du processus :
- 0 :
STDIN(par défaut : le clavier) ; - 1 :
STDOUT(par défaut : l’écran) ; - 2 :
STDERR(par défaut : l’écran).
- 0 :
1.2 PID, PPID, groupes de processus et numéros de session
Au niveau du système, tout processus est désigné par un entier positif, le PID (le premier processus,1.3 Utilisateurs et groupes réels et effectifs
L’utilisateur et le groupe réels d’un processus sont l’utilisateur et le groupe qui l’ont appelé. Cette identification permet de gérer la comptabilité du système (pour savoir qui utilise quelles ressources et, éventuellement, facturer). L’utilisateur et le groupe effectifs d’un processus sont ceux sous lesquels s’exécute un processus (ce sont eux deux qui fixent les droits du processus). L’utilisateur réel% ps a -ouser,ruser,command|grep [p]asswd root jaco passwdCette distinction permet à l’utilisateur lambda d’effectuer des commandes qui nécessitent d’avoir l’identité d’un autre utilisateur (
1.4 Création et suppression de processus
La création des processus est dynamique.1.4.1 Création
Un processus est créé par un appel à- Chacun a son propre segment de données et son propre segment de pile, qui contiennent la même chose au départ.
- Chacun a son propre environnement, qui contient la même chose au départ.
- Chacun a ses propres descripteurs de fichiers : ce sont les mêmes au départ (le processus fils doit donc fermer les descripteurs qui ne le concernent pas).
- Le père et le fils ont le même code dans leurs segments de code : ils exécutent donc le même programme (et le fils exécute donc également l’appel
fork(2)...)
- -1 en cas d’erreur (consulter alors
errno) ; - 0 chez le fils ;
- > 0 chez le père (en fait, le PID du processus fils créé).
switch ( pid = fork() ) {
case -1 : /* traiter l’erreur */
...
break;
case 0 : /* traitement à exécuter par le fils */
...
break;
default : /* traitement à exécuter par le père */
...
}
En Ruby, le module - Sans bloc associé,
forkcrée un processus fils et renvoie son PID au processus père ounilau processus fils (commefork(2)). En cas d’échec, cette méthode lèvera une exception. - Avec un bloc, l’appel renverra le pid du processus fils, qui exécutera les instructions de ce bloc, pendant que le père exécutera celles qui suivent le bloc. Le fils se terminera automatiquement à la fin du bloc en renvoyant 0 (succès).
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int pid;
switch(pid = fork()) {
case -1: /* Pb... */
perror(“Erreur du fork”);
exit(1);
case 0: /* On est chez le fils */
printf(“Je suis le fils, mon PID est %d,”
“ le PID de mon père est %d\n",
getpid(), getppid());
break;
default: /* On est chez le père */
printf("Je suis le père, mon PID est %d\n",
getpid());
}
exit(0);
}
La même chose en Ruby, sans bloc :
if (fork == nil)
puts(“Je suis le fils : #{Process.pid}, mon pere est #{Process.ppid}”)
else
puts(“Je suis le pere : #{Process.pid}”)
end
Idem, avec un bloc :
fork do # Code exécuté par le fils
puts("Je suis le fils: #{Process.pid}, mon pere est #{Process.ppid}")
end
# Code exécuté par le père
puts("Je suis le pere : #{Process.pid}")
Petit jeu amusant : combien de lignes s’afficheront lorsque l’on exécute le programme suivant ?
1.upto(4) do |i|
if fork != nil
puts "pid = #{Process.pid}, i = #{i}"
end
end
Suite du jeu : donner la relation entre le nombre de boucles et le nombres de lignes affichées.
1.4.2 Terminaison d’un processus
Un processus se termine par l’appel à#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
/* Test de création d’un processus :
on crée un fils. Le père attend la fin de
celui-ci en testant la condition de sa fin. On
peut simuler une mauvaise fin en effectuant
un ‘kill’ du processus fils.
*/
int main(void) {
pid_t pid = fork();
int raison;
switch (pid) {
case -1 : perror(“Erreur de création du processus");
exit(1);
case 0 : /* on est chez le fils */
printf("Pid du fils = %d\n", getpid());
sleep(20);
break;
default : /* on est chez le père */
printf("Pid du père = %d\n", getpid());
printf("Attente de la terminaison du fils...\n");
pid = wait(&raison);
if (raison == 0)
printf("\a%d s’est terminé correctement...\n", pid);
else printf("\a\a%d s’est mal terminé : %d\n",
pid, raison);
} /* switch */
exit(0); /* exécuté par le fils et le père
}
En Ruby, on dispose également des méthodes do
puts("PID du fils : #{Process.pid}")
sleep(20)
end
puts("PID du pere : #{Process.pid}")
puts(“Attente de la terminaison du fils...”)
pid = Process.wait
if $?.exitstatus == 0
puts(“#{pid} s’est correctement termine”)
else
puts(“#{pid} s’est mal termine”)
end
exit(0) # uniquement par le père
Pour empêcher les processus zombies, on peut également détourner fork do
puts("PID du fils : #{Process.pid}")
end
puts(“PID du pere : #{Process.pid}”)
puts(“Mon fils est un zombie pendant 20 secondes (vérifier avec ps)...")
sleep(20)
pid = Process.wait
if $?.exitstatus == 0
puts(“#{pid} s’est correctement terminé")
else
puts("#{pid} s’est mal termine")
end
exit(0) # uniquement par le père
Voici la solution pour éviter les zombies (on notera le remplacement du pid = fork do
puts("PID du fils : #{Process.pid}")
end
trap(:SIGCHLD) do
Process.waitpid
end
puts("PID du pere : #{Process.pid}")
puts(“Mon fils n’est plus un zombie (vérifier avec ps)...")
gets # Pour pouvoir vérifier qu’il n’y a pas de zombie
begin
Process.waitpid # 2
rescue Errno::ECHILD
puts("fini")
ensure
exit(0) # uniquement par le père
end
Remarque :
Ce n’est pas parce qu’on essaie d’éviter les processus zombies qu’il faut créer des processus orphelins ! Si, pour une raison donnée, le processus arrive sur le pid = fork do
puts("PID du fils : #{Process.pid}")
puts("Mon PPID : #{Process.ppid}")
end
puts(“PID du pere : #{Process.pid}”)
Process.detach(pid)
puts(“Mon fils n’est plus un zombie pendant 20 sec (vérifier avec ps)...")
sleep(20) # pour pouvoir vérifier qu’il n’y a pas de zombie
begin
Process.waitpid
rescue Errno::ECHILD
puts("fini")
ensure
exit(0) # uniquement par le père
end
Ici, on évite à la fois les zombies et les orphelins. Pour les mêmes raisons que précédemment, le 1.5 Commutation d’image
Comme on l’a vu, après un appel àpathest le nom du programme dont on veut charger le code.argvest un tableau contenant les paramètres passés à ce programme (argv[0]doit être le nom du programme).envpest un tableau contenant les chaînes d’environnement du programme. La variable globale environ contient un pointeur vers ce tableau.
switch (fork()) {
case -1 : /* traiter l’erreur */
case 0 : /* on est chez le fils, on exécute un autre code */
...
exec...(....);
* traiter l’erreur de l’exec */
default: /* on est chez le père */
...
}
En Ruby, on dispose de la méthode begin
raise "Usage: #{$0} nomfic" if not ARGV[0] or ARGV.length > 1
fork do # processus fils
fic = File.open("errtmp", "w")
STDERR.reopen(fic) # redirection de STDERR
fic.close
exec(“gcc”, ARGV[0]) # exécution de gcc
end
# Code du père
Process.wait # attend la fin de gcc
if $?.exitstatus != 0 # erreurs de compilation
fork do # création d’un fils
exec("more", "errtmp") # visualisation des erreurs
end
# code du père
Process.wait
print("Appuyez sur Entree pour lancer l’éditeur :")
STDIN.gets
exec("vi", ARGV[0]) # puis chargement de vi
else # la compilation s’est bien passée
exec("./a.out") # on exécute le programme
end
rescue RuntimeError => e
STDERR.puts(e)
exit(1)
rescue Exception => e
STDERR.puts(e)
exit(2)
end
On pourrait encore trouver bien d’autres exercices amusants : par exemple une compilation des différentes sources d’un programme en parallèle (avec 2. Les tubes de communication (pipes)
La plupart des commandes Unix sont des filtres. Un filtre est un programme qui prend des données en entrée (sur STDIN), les transforme et affiche le résultat sur sa sortie (- synchronisation automatique pour l’accès au tube :
- lecture bloquante si le nombre d’informations demandé n’est pas disponible ;
- écriture bloquante si le tube ne peut contenir les informations à écrire.
- la gestion des requêtes n’est pas nécessairement FIFO (d’où des problèmes de famine) ;
- la lecture se fait dans l’ordre des écritures (FIFO) ; il n’y a qu’une seule consommation (destructrice).
2.1 Tubes vers d’autres processus
La fonctionbegin
# Ouverture du tube pour *lire* la sortie de "ps -l"
IO.popen("ps -l") do |f|
f.readlines.each { |ligne| puts ligne}
end
rescue Exception => e
STDERR.puts(e)
exit(1)
end
Un code plus court (mais s’éloignant du codage " traditionnel ") :
puts IO.popen("ps -l").read
Exemple de tube en écriture :
require ‘etc’
# Création de la commande
destinataire = Etc.getpwuid.name
commande = "mail -s ‘message du processus #{Process.pid}’ #{destinataire}"
# Ouverture d’un tube pour *écrire* dans cette commande
begin
IO.popen(commande, "w") do |f|
f.puts(“C’est la commande #{$0} qui vous parle”)
f.puts(“J’agis pour le compte de #{Etc.getpwuid.gecos}”)
end
puts(“Message envoyé à #{destinataire}")
rescue Exception => e
STDERR.puts(e)
exit(1)
end
REMARQUE :
La construction puts %x(ps -l) # ou : puts `ps -l`Le module
require ‘open3’
Open3.popen3(une_commande) do |stdin, stdout, stderr|
err = stderr.gets
if err
puts "#{err}"
else
stdout.readlines.each { |x| puts x}
end
end
2.2 Tubes système
Un tube est créé dynamiquement par un processus à l’aide de la fonction2.2.1 Accès à un tube
Les accès à un tube sont identiques à ceux des fichiers et utilisent donc les appels2.2.2 Fin de communication
Dans le cas d’un consommateur (lecteur), elle a lieu lorsqu’il n’y a plus aucun processus ayant le tube ouvert en écriture. Le consommateur récupère alors ce qui reste dans le tube, à concurrence de ce qu’il avait demandé :- Nb_Restants >= Nb_Octets => Nb_Octets
- Nb_Restants < Nb_Octets => Nb_Restants
2.2.3 Tubes en Ruby
La classe IO fournit la méthode2.2.4 Exemple
On veut écrire un script qui effectue le pipeline begin
rd, wr = IO.pipe # Creation du pipe sort-wc
fork do # Le fils fait le sort
rd.close # il n’y lira jamais
STDOUT.reopen(wr) # redirection de la sortie standard dans le tube
wr.close
rd, wr = IO.pipe # creation du pipe grep-sort
fork do # le fils fait le grep
rd.close # idem sort
STDOUT.reopen(wr)
wr.close
exec("grep", ARGV[0], ARGV[1], ARGV[2])
end
# le père fait le sort
wr.close
STDIN.reopen(rd)
rd.close
exec("sort", "-uf")
end
# le père de fait le wc
wr.close
STDIN.reopen(rd)
rd.close
exec("wc", "-l")
rescue Exception => e
STDERR.puts(e)
exit(1)
end
2.3 Les tubes nommés (FIFO)
Les tubes système n’ont pas de nom, ils ne peuvent donc être partagés qu’entre des processus ayant un ancêtre commun. Autrement dit, deux processus distincts ne peuvent pas utiliser un tube commun pour communiquer. Une FIFO est un tube auquel on a associé un nom, il est donc accessible par tous les processus, qui pourront l’utiliser pour communiquer.2.3.1 Création, ouverture et fermeture d’une FIFO
On crée un tube nommé à l’aide de la fonction2.3.2 Écriture et lecture d’une FIFO
Les écritures et les lectures d’une FIFO utilisent les appels2.3.3 Exemple : communication entre deux processus distincts
On a deux processus, client et serveur, désirant communiquer : on utilise deux FIFO,
def serveur(lect, ecr)
begin
nom_fic = lect.gets.chomp
File.foreach(nom_fic) do |ligne|
ecr.puts(ligne)
end
rescue Exception => e
ecr.puts(“Pb d’ouverture du fichier”)
end
end
FIFO_1 = "/tmp/FIFO.1"
FIFO_2 = "/tmp/FIFO.2"
# Création des 2 FIFO
system(“mkfifo -m 0666 #{FIFO_1}”) if not File.exists?(FIFO_1)
system(“mkfifo -m 0666 #{FIFO_2}”) if not File.exists?(FIFO_2)
begin
# Ouverture des FIFO
readfd = File.open(FIFO_1, "r")
writefd = File.open(FIFO_2, "w")
serveur(readfd, writefd)
rescue Exception => e
STDERR.puts(e)
exit!(1)
end
Code du client def client(lect, ecr)
print("Entrez un nom de fichier :")
nom_fic = gets.chomp
ecr.puts(nom_fic)
ecr.flush
while ligne = lect.gets
puts ligne
end
end
FIFO_1 = "/tmp/FIFO.1"
FIFO_2 = "/tmp/FIFO.2"
begin
# Ouverture des FIFO : attention à
# l’ordre, sinon deadlock !!!
writefd = File.open(FIFO_1, "w")
readfd = File.open(FIFO_2, "r")
client(readfd, writefd)
rescue Exception => e
STDERR.puts(e)
exit!(1)
ensure
writefd.close
readfd.close
File.unlink(FIFO_1, FIFO_2)
end
2.3.4 Exemple : un serveur, plusieurs clients
Principe :- Le serveur crée une FIFO portant un nom qui devra être connu de tous les clients. Cette FIFO est ouverte en lecture, elle servira de boîte de réception au serveur.
- Chaque client crée sa propre FIFO d’écriture, dont le nom est formé d’après son PID.
- Une requête arrivant au serveur devra contenir le PID du client en plus de la requête proprement dite : le serveur sait alors dans quelle FIFO il doit écrire.

- Lancer un ensemble de fils au démarrage du serveur et les bloquer. On débloque un fils pour chaque nouvelle requête.
- Ne pas créer de processus fils, mais utiliser des threads (on crée un thread par client).
- Créer un ensemble de threads au démarrage et les bloquer. On débloque un thread pour chaque nouvelle requête.
3. Accès concurrents à des objets partagés
3.1 Problème de l’exclusion mutuelle
3.1.1 Énoncé du problème
Soit un système de réservation de billets d’avion. Le code permettant de réserver une place pourrait être :Trouver une place libre; Nb_Places_Reservées := Nb_Places_Reservées + 1;Ce code peut être exécuté par plusieurs processus en même temps (agences de réservation) et
- un prologue pour attendre de pouvoir entrer en section critique ;
- un épilogue pour signaler qu’on en est sorti.
Entrée -------------> blocage du processus tant que la SC est occupée Section Critique Sortie -------------> on signale qu’on a libéré la SCIl ne doit pas être possible d’entrer en section critique (ou en sortir) sans passer par Entrée (ou Sortie).
3.1.2 Spécification d’une solution valide
- À tout instant, un processus au plus est engagé en SC.
- Aucune hypothèse ne sera faite sur la vitesse des processus.
- Tout processus est susceptible d’être interrompu à tout moment.
- Pas de famine.
- Si la SC est libre, tout processus demandant à y entrer doit pouvoir le faire.
3.2 Les sémaphores de Dijkstra (1965)
3.2.1 Présentation
Un sémaphore est un objet abstrait sur lequel on peut réaliser deux opérations atomiques :- un compteur
c, qui est un entier représentant la valeur du sémaphore ; - une file d’attente
f, initialement vide.
3.2.2 Fonctionnement
L’algorithme deS.c := S.c - 1; SI S.c < 0 ALORS Insérer le processus exécutant P dans S.f ; Bloquer ce processus ; FIN SI;L’algorithme de
S.c := S.c + 1; SI S.c <= 0 ALORS /* il y a des processus bloqués */ Extraire le processus en tête de S.f; Débloquer ce processus ; FIN SI;On remarque que le nombre de processus bloqués par un sémaphore
3.2.3 Utilisation des sémaphores
Réalisation de l’exclusion mutuelle : on utilise un sémaphore, traditionnellement appelé MUTEX (MUTual EXclusion) :MUTEX : Semaphore;
Sem_Init(MUTEX, 1); /* 1 seul process doit pouvoir entrer */
P(MUTEX); /* donc MUTEX.c vaut maintenant 0 */
/* tout process faisant P(MUTEX) sera bloqué */
SC /* Section Critique */
V(MUTEX); /* MUTEX.c repasse à 1 */
Note :
MUTEX ne doit servir qu’à cette exclusion mutuelle !
Problème du producteur-consommateur : un processus require ‘semaphore’
N = 100
vide = semaphore.new(N)
plein = semaphore.new(0)
mutex = semaphore.new(1)
def producteur
loop do
objet = ... # Création d’un objet
vide.p # Une case vide de moins
mutex.p # Entrée en SC
mettre(objet) # Met l’objet dans le tampon
mutex.v # Sortie de la SC
plein.v # Une case pleine de plus
end
end
def consommateur
loop do
plein.p # Une case pleine de moins
mutex.p # Entrée en SC
prendre(objet) # retirer l’objet du tampon
mutex.v # Sortie de SC
vide.v # Une case vide de plus
consommer(objet) # Utiliser l’objet
end
end
3.2.4 Un grand classique : le problème des philosophes (Dijkstra)
Des philosophes sont assis autour d’une table ronde, devant une assiette. Chaque philosophe a exactement une fourchette à sa gauche et une fourchette à sa droite. Les philosophes pensent et mangent tour à tour. Problème : comment s’assurer qu’ils pourront tous manger et dans les meilleurs délais ? Ce problème modélise l’accès concurrent à des ressources en nombre limité (périphériques d’E/S, par exemple). Première solution :N = 5 # Nombre de philosophes
def philosophe(i)
loop do
penser
prendre_fourchette(i) # fourchette gauche
prendre_fourchette(i + 1) # fourchette droite
manger
poser_fourchette(i)
poser_fourchette(i + 1)
end
end
Cette solution ne marche pas... si chaque philosophe prend en même temps sa fourchette gauche, il y aura interblocage.
Solution possible : un sémaphore par philosophe pour contrôler l’attente des deux fourchettes libres simultanément (problème de synchronisation) :
- Un philosophe a 0 ou 2 fourchettes (jamais une seule).
- Un philosophe prévient ses voisins quand il libère ses fourchettes.
- Un philosophe a trois états : PENSE, FAIM, MANGE.
- Une fourchette est libre si l’autre philosophe n’est pas dans l’état MANGE.
- Pas d’interblocage car un philosophe prend ses deux fourchettes d’un coup et bloque donc ses deux voisins.
require ‘semaphore’
N = 5 # Nombre de philosophes
def gauche(i)
return (i - 1) % N
end
def droite(i)
return (i + 1) % N
end
etats = []
sems = []
mutex = semaphore.new(1)
# Initialisation du tableau des 5 semap
1.upto(N) { sems << semaphore.new(0) }
hores
def test(i)
if etats[i] == :FAIM and etats[gauche(i)] != :MANGE and
etats[droite(i)] != :MANGE
etats[i] = :MANGE
sems[i].v
end
def prendre_fourchettes(i)
mutex.p
etats[i] = :FAIM
test(i) # Essaie d’avoir deux fourchettes
mutex.v
sems[i].p # Blocage si pas deux fourchettes
end
def poser_fourchettes(i)
mutex.p
etats[i] = :PENSE
test(gauche(i)) # Autorise gauche à manger
test(droite(i)) # Autorise droite à manger
mutex.v
end
def philosophe(i)
loop do
penser
prendre_fourchettes(i)
manger
prendre_fourchettes(i)
end
end
3.2.5 Les sémaphores en Ruby
Il n’existe pas de classe=begin
jsemaphore.rb : implémentation d’une classe Semaphore
en utilisant des pipes pour la
synchronisation
=end
class Semaphore
def initialize(cpteur = 1)
raise ArgumentError, "cpteur must be >= 0" if cpteur < 0
@rd, @wr = IO.pipe
cpteur.downto(1) { @wr.putc(:jeton) }
end
def p { @rd.getc }
def v { @wr.putc(:jeton) }
def synchronize
self.p
begin
yield
ensure
self.v
end
end
# Mettre en ObjectSpace.define_finalizer ?
def delete
@rd.close
@wr.close
end
end
#---------------------------------------
if __FILE__ == $0
begin
sem_ping = Semaphore.new(1)
sem_pong = Semaphore.new(0) # Bloque tout de suite les Pong...
1.upto(10) do
fork do
sem_pong.p
puts("Pong")
sem_ping.v
end
end
1.upto(10) do
sem_ping.p
print("Ping...")
STDOUT.flush
sem_pong.v
end
ensure
sem_ping.delete
sem_pong.delete
end
3.3 Les processus légers (threads)
3.3.1 Présentation
Les interfaces graphiques, les systèmes multiprocesseurs ainsi que les systèmes répartis ont donné lieu à une révision de la conception usuelle des processus. Cette révision se fonde sur la notion de fils d’exécution (thread of control). Pour comprendre la différence entre les processus classiques d’Unix (processus lourds) et les threads (processus légers), il suffit de comparer la gestion des processus Unix avec celle des tâches Mach. Comme on l’a vu, un processus Unix contient un programme en cours d’exécution et un ensemble de ressources, comme sa table des descripteurs de fichiers et un espace adressable. Une tâche Mach, par contre, ne contient qu’un ensemble de ressources, ce sont les threads qui gèrent toutes les activités d’exécution. À une tâche Mach peut être associé un nombre quelconque de threads et tout thread doit être associé à une tâche. Tous les threads associés à une tâche donnée partagent les ressources de celle-ci : un thread est donc essentiellement un compteur de programme, une pile et un ensemble de registres, toutes les autres structures de données appartiennent à la tâche. En fait, on peut modéliser un processus Unix par une tâche mono-thread. L’idée est donc d’associer plusieurs fils d’exécution à un espace d’adressage et à un processus. Les fils d’exécution sont donc des processus dégradés (sans espace d’adressage propre) à l’intérieur d’un processus ou d’une application.3.3.2 Intérêt des processus légers
Certaines applications se dupliquent entièrement au cours du traitement. C’est notamment le cas pour des systèmes client-serveur, où le serveur exécute un3.3.3 Création et terminaison d’un thread
L’utilisation de ces primitives passe par l’inclusion de3.3.4 Un exemple...
On crée ici une application utilisant vingt threads : 10 affichant " ping ", 10 affichant " Pong ". Note : Ne pas mettre les join. On y reviendra plus tard.=begin
ping_pong.rb : présentation des threads et de leur
synchro par un mutex. Comme on ne peut
pas préciser le nombre de tickets à la
création d’un objet Mutex, on le bloque
dès le départ.
=end
require ‘thread’
WITH_SEMAPHORES = (ARGV[0] != ‘--without-sem’)
if WITH_SEMAPHORES
sem_ping = Mutex.new
sem_pong = Mutex.new
# blocage d’entrée du thread pong
sem_pong.lock
end
threads_pong = []
1.upto(10) do # Création de 10 threads pong
threads_pong << Thread.new do
sem_pong.lock if WITH_SEMAPHORES
puts(“pong...”)
sem_ping.unlock if WITH_SEMAPHORES
end
end
threads_ping = []
1.upto(10) do
threads_ping << Thread.new do
sem_ping.lock if WITH_SEMAPHORES
print(“Ping...”)
sem_pong.unlock if WITH_SEMAPHORES
end
end
# Voir plus loin...
threads_ping.each { |t| t.join}
threads_pong.each { |t| t.join}
puts(“”) # Pour afficher les Ping si pas de synchro
Pour information, la même chose en C :
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>
#define WITH_SEMAPHORES 1
sem_t sem_ping, sem_pong;
void *func_ping(void *bidon) {
if (WITH_SEMAPHORES) sem_wait(&sem_ping);
printf(„Ping... „); fflush(stdout);
if (WITH_SEMAPHORES) sem_post(&sem_pong);
return NULL;
}
void *func_pong(void *bidon) {
if (WITH_SEMAPHORES) sem_wait(&sem_pong);
printf(„Pong\n“); fflush(stdout);
if (WITH_SEMAPHORES) sem_post(&sem_ping);
return NULL;
}
int main(void) {
int i;
pthread_t ping, pong;
if (WITH_SEMAPHORES) sem_init(&ping, 0, 1);
if (WITH_SEMAPHORES) sem_init(&pong, 0, 0);
for (i = 0; i < 10; i++)
pthread_create(&pong, NULL, func_pong, NULL);
for (i = 0; i < 10; i++)
pthread_create(&ping, NULL, func_ping, NULL);
/* Voir plus loin */
for (i = 0; i < 10; i++) {
pthread_join (hello, NULL);
pthread_join (world, NULL);
}
putchar(‘\n’);
if (WITH_SEMAPHORES) sem_destroy(&ping);
if (WITH_SEMAPHORES) sem_destroy(&pong);
exit(0);
}
3.3.5 Le thread initial (principal)
Lorsqu’une application démarre, il y a lancement automatique d’un thread pour exécuter{ int result = main(argc, argv); exit(result); }
Par conséquent, quand - Bloquer le thread initial sur l’attente de la terminaison d’un ou plusieurs autres threads (avec
pthread_join). - Terminer explicitement le thread initial avec
pthread_exit, ce qui court-circuitera l’appel àexit.
3.3.6 Synchronisation par verrous
Les threads partageant des données communes, on doit assurer la cohérence des accès à celles-ci en utilisant les mécanismes de synchronisation. La norme POSIX prévoit deux types de synchronisation : les verrous d’exclusion (sémaphores d’exclusion mutuelle ou mutex) et les variables conditions (tirées des moniteurs de Hoare).3.3.6.1 Création dynamique ou statique d’un verrou d’exclusion
Un mutex peut être créé dynamiquement par un appel àpthread_mutex mutex = PTHREAD_MUTEX_INITIALIZER;Dans le cas de la création dynamique, on utilisera le plus souvent la constante
3.3.6.2 Suppression d’un verrou
Pour supprimer un verrou, on utilise l’appel3.3.6.3 Verrouillage et déverrouillage
On verrouille par un appel à3.3.7 Synchronisation par variables conditions
3.3.7.1 Création dynamique ou statique d’une variable condition
Comme les verrous d’exclusion mutuelle, les variables conditions peuvent être créées de façon dynamique, avec un appel à3.3.7.2 Destruction
Une variable condition est détruite par un appel à3.3.7.3 Blocage
Pour bloquer un thread sur l’attente d’une condition, on utilise l’appel à3.3.7.4 Réveil simple ou général
L’appel3.3.8 Exemples
Implantation d’un mécanisme producteur/consommateur dans un tampon d’une case en utilisant les threads :#include <pthread.h>
#include <stdio.h>
#include <sys/types.h>
char tampon;
int tampon_plein = 0;
pthread_mutex_t mutex;
void lecture(void);
void ecriture(void);
int main(void) {
pthread_t lecteur;
pthread_mutex_init( &mutex, pthread_mutexattr_default );
pthread_create( &lecteur, pthread_attr_default,
(void *)&lecture, NULL );
ecriture(); /* C’est le thread principal qui écrit */
return 0;
}
void ecriture(void) {
for( ;; ) {
pthread_mutex_lock( &mutex );
if ( !tampon_plein ) {
printf( “Écriture dans tampon\n" );
tampon_plein = 1;
}
pthread_mutex_unlock( &mutex );
}
}
void lecture(void) {
for( ;; ) {
pthread_mutex_lock( &mutex );
if ( tampon_plein ) {
printf( “lecture dans tampon\n” );
tampon_plein = 0;
}
pthread_mutex_unlock( &mutex );
}
}
En Ruby :
require ‘thread’
tampon_plein = false
tampon = nil
mutex = Mutex.new
id = Thread.new do # Thread de lecture
loop do
mutex.lock
if tampon_plein
puts(“Lecture dans tampon”)
tampon_plein = false
end
mutex.unlock
end
end
# Le thread principal écrit
loop do
mutex.lock
if not tampon_plein
puts(“Ecriture dans tampon”)
tampon_plein = true
end
mutex.unlock
end
id.join
Implantation d’un mécanisme producteur/consommateur dans un tampon à N cases en utilisant les threads :
#include <pthread.h>
#include <stdio.h>
#define N 10
pthread_mutex_t mutex;
pthread_cond_t est_libre, est_plein;
char Tampon[N];
int Nb_In; /* Nbre de caractères dans Tampon */
void *ecrire(void) {
pthread_mutex_lock(&mutex);
while (Nb_In == N) /* Tampon plein : on attend */
pthread_cond_wait(&est_libre, &mutex);
Tampon[Nb_In++] = ‘c’; /* on écrit */
printf("Un caractère ecrit...\n");
pthread_cond_signal(&est_plein); /* il y a qquechose à lire */
pthread_mutex_unlock(&mutex);
}
void *lire(void) {
char car;
pthread_mutex_lock(&mutex);
while (Nb_In == 0) /* Tampon vide : on attend */
pthread_cond_wait(&est_plein, &mutex);
car = Tampon[Nb_In--] ; /* on lit */
printf(“Un caractère lu...\n");
pthread_cond_signal(&est_libre); /* il y a une place libre */
pthread_mutex_unlock(&mutex);
}
int main(void) {
int i;
pthread_t producteur, lecteur;
pthread_mutex_init(&mutex, pthread_mutexattr_default);
pthread_cond_init(&est_libre, pthread_condattr_default);
pthread_cond_init(&est_plein, pthread_condattr_default);
Nb_In = 0;
for (i = 0; i < 1000; i++)
pthread_create(&lecteur, NULL, (void *)&lire, NULL);
for (i = 0; i < 1000; i++)
pthread_create(&producteur, NULL, (void *)&ecrire, NULL);
for (i = 0; i < 1000; i++) {
pthread_join(producteur, NULL);
pthread_join(lecteur, NULL);
}
return 0;
}
La même chose en Ruby :
require ‘thread’
N = 10
mutex = Mutex.new
est_libre = ConditionVariable.new
est_plein = ConditionVariable.new
nb_in = 0
tampon = []
th_prods = []
th_lects = []
1.upto(1000) do
th_lects << Thread.new do # Thread des lecteurs
mutex.lock
est_plein.wait(mutex) while nb_in == 0 # rien à lire
car = tampon[nb_in]
nb_in -= 1
puts(“un caractère lu")
est_libre.signal # il y a une case de libre
mutex.unlock
end
end
1.upto(1000) do
th_prods << Thread.new do # Thread des producteurs
mutex.lock
est_vide.wait(mutex) while nb_in == N # Tampon plein
tampon[nb_in] = ‘c’
nb_in += 1
puts(“un caractère écrit")
est_plein.signal # il y a une case à lire
mutex.unlock
end
end
th_lects.each {|id| id.join}
th_prods.each {|id| id.join}
4. Ressources
Si Ruby n’est pas déjà installé sur votre système, vous pouvez utiliser votre gestionnaire de paquetage ou télécharger les sources et les compiler vous-même (ce qui n’est pas bien difficile puisque cela consiste essentiellement à exécuter le trio infernal cRetrouvez cet article dans : Linux Magazine 86





Donnez votre avis
Vous devez avoir ouvert une session pour écrire un commentaire.