Programmation parallèle§
Systèmes d’exploitation
auteur: | Pierre-Antoine Champin |
---|---|
adresse: | Département Informatique - IUT - Lyon1 |
licence: | ![]() |
Systèmes d’exploitation
auteur: | Pierre-Antoine Champin |
---|---|
adresse: | Département Informatique - IUT - Lyon1 |
licence: | ![]() |
Certaines applications peuvent avoir besoin d’effectuer plusieurs traitements en parallèle. Elles peuvent être constituées de plusieurs processus, bénéficiant ainsi:
Mais cette solution présente aussi des inconvénients :
Un thread (ou fil d’exécution) est l’exécution d’une procédure/fonction en parallèle de la fonction principale d’un processus
Un processus comporte un ou plusieurs threads.
Le PCB du processus contient pour chaque thread:
En revanche, toutes les autres ressources (mémoire, fichiers, etc…) sont partagées par tous les threads du processus;
→ c’est au programmeur de les arbitrer
Les threads sont souvent appelés « processus légers ».
Ils bénéficient des mêmes avantages en terme de parallélisme, que les processus (l’ordonnanceur considère chaque thread individuellement).
Avantages par rapport aux processus :
void process_partial(section_t* sec) {
// traite la section du tableau spécifiée par sec
// ...
}
void process_all(int *tab) {
pthread_t t;
// traite la 1e moitié dans un thread séparé t
pthread_create(&t, NULL, process_partial,
section(tab, 0, SIZE/2));
// traite la 2e moitié dans ce thread
process_partial(section(tab, SIZE/2, SIZE));
// attend la fin du thread t
pthread_join(t, NULL);
}
Dans certains contextes, les threads ne sont pas gérés par le noyau, mais émulés par un programme (par exemple la VM Java),
- qui s’exécute en mode utilisateur (d’où l’appellation thread utilisateur, qu’on oppose aux threads noyaux) ;
- et économise le temps de passer par le noyau (d’où l’appellation green thread).
Note
Concernant les appels systèmes bloquants, les bibliothèques et les VM qui implémentent les threads utilisateurs fournissent des fonctions à utiliser à leur place.
Ces fonctions sont pseudo-bloquantes, dans le sens ou elles « bloquent » le thread seul (pour leur ordonnanceur interne), mais utilisent en interne un appel non bloquant.
La commutation préemptive entre les threads pose certains problèmes :
- coûteuse en temps (surtout lorsqu’elle est gérée par le noyau),
- pose de nombreux problèmes de synchronisation,
- imposant des solutions de verouillages également pénalisant en temps.
Des coroutines sont des fonctions qui ont vocation
Disponibles dans de nombreux langages :
SwitchToFiber
) ;yield
) ;def chaine_de_traitement():
for i in range(10):
obj = produit(i)
if obj.val == 0:
continue
elif obj.val == 2:
consomme(obj[0])
consomme(obj[1])
else:
consome(obj)
def producteur():
for i in range(10):
yield produit(i)
def transformateur():
for obj in producteur():
if obj.val == 0:
continue
elif obj.val == 2:
yield obj[0]
yield obj[1]
else:
yield obj
def consommateur():
for obj in transformateur():
consomme(obj)
def producteur():
for i in range(10):
yield produit(i)
def transformateur():
for obj in producteur():
if obj.val == 0:
continue
elif obj.val == 2:
yield obj[0]
yield obj[1]
else:
yield obj
def consommateur():
for obj in transformateur():
consomme(obj)
def handle_request(req):
rep1 = calcule_debut_reponse(req)
req.send(rep1) # rend la main
rep2 = calcule_fin_reponse(req, rep1)
impacte_donnees_locales(rep1, rep2)
req.send(rep2) # rend la main
Note
À l’avant-dernière ligne, on note qu’on modifie les données locales au serveur ; avec des threads, cette procédure comporterait des sections critiques, et devrait donc avoir recours à des mutex.
Avec des co-routines :