282 lines
8.5 KiB
TeX
282 lines
8.5 KiB
TeX
%\part{Cours 1\er}
|
|
|
|
\chapter{Introduction}
|
|
|
|
\section{Motivations}
|
|
|
|
On augmente plus trop en fréquence, ça augmente plus trop ces dernières années
|
|
comparé à avant. Et pourtant, on augmente toujours le nombre de processeurs sur
|
|
les puces. On va de plus en plus vers du parallélisme et non plus vers de la
|
|
vitesse.
|
|
|
|
\section{Vocabulaire}
|
|
|
|
\subsection{Modèle d'exécution}
|
|
|
|
\subsubsection{À acteurs}
|
|
|
|
Ils communiquent entre-eux~: au lieu d'éxécuter le code d'un objet dans le
|
|
thread courant, la méthode s'éxécute dans le thread de l'objet. Similaire au
|
|
RPC.
|
|
|
|
\subsubsection{Futures}
|
|
|
|
Notion d'éxécution lazy. Le calcul commence en tâche de fond, lorsque l'on a
|
|
besoin de la valeur, on est mis en attente si elle n'est pas encore calculée~;
|
|
on a donc pas à attendre au début du calcul.
|
|
|
|
\subsubsection{CSP -- Communication Séquential Processors}
|
|
|
|
Modèle dans lequel on a des processus séquentiel qui ne font que se parler (pas
|
|
de mémoire partagée).
|
|
|
|
\subsubsection{CCS}
|
|
|
|
?
|
|
|
|
\subsection{Gestion des sections critiques}
|
|
|
|
\subsubsection{Locks}
|
|
\subsubsection{Conditions}
|
|
\subsubsection{Mutex}
|
|
\subsubsection{Sémaphores}
|
|
|
|
\subsection{Histoire}
|
|
|
|
Apparu vers la fin des années 50. Premier aboutissement (machine à 4
|
|
processeurs) en 1962.
|
|
|
|
\subsection{Définitions}
|
|
|
|
\subsubsection{Matérielle}
|
|
|
|
\paragraph{Symetric MultiProcessor} On place plusieurs processeurs (identiques
|
|
ou non) sur une carte mère spéciale.
|
|
|
|
\paragraph{Multi-core} deux processeurs ou plus en un, avec certain composants
|
|
partagés~: en particulier les caches. Moins cher que du SMP
|
|
|
|
\paragraph{Hyper-threading} Étant donné que chaque famille d'instruction a une
|
|
zone réservée dans le processeur, du coup, lorsqu'une instruction est en train
|
|
de s'exécuter, elle bloque le reste du processeur, du coup, beaucoup de circuit
|
|
ne servent pas, l'hyperthreading essaye d'exécuter plusieurs zone en même
|
|
temps.
|
|
|
|
\paragraph{Vectorized instructions} applique une instruction sur un lot de
|
|
donnée.
|
|
|
|
\paragraph{Non-Uniform Memory Access (\textsc{numa})} on associe à chaque
|
|
processeur des blocs mémoires. Un certain processeur a un accès prioritaire à
|
|
ses blocs mémoires.
|
|
|
|
|
|
\chapter{Soyons parallèle}
|
|
|
|
\section{Gain~?}
|
|
|
|
Amdahl's law~: $$\frac{1}{(1-P\frac{P}{N}}$$
|
|
|
|
On n'ira jamais deux fois plus vite que deux fois plus vite avec 50\% du code
|
|
parallélisé. Au final, il faut s'arranger sur la parallélisation du code,
|
|
plutôt que sur le nombre de processeurs.\\
|
|
|
|
Gustafson's law~: $$S(P)=P+a\times(P-1)$$
|
|
|
|
\section{Différents types de parallélisme}
|
|
|
|
\subsection{Décomposition par tâches}
|
|
|
|
On lance un thread par tâche en gros. Il faut voir les problèmes
|
|
d'interdépendances entre chaque thread, etc.
|
|
|
|
\subsection{Data driven}
|
|
|
|
On a un paquet de données similaire. On a un traitement similaire à effectuer
|
|
sur ces données.
|
|
|
|
On découpe les données en petits blocs et on lance plein de petit threads.
|
|
|
|
\subsection{Modèle flow}
|
|
|
|
Comme une chaîne de production. On a $n$ tâche, chaque unité traite une
|
|
tâche. Pour que ça marche, il faut que les données soient correctement découpés.
|
|
|
|
\subsection{Design Patterns}
|
|
|
|
\paragraph{Divide and Conquer} on divise le problème, puis on a une seconde
|
|
phase de recollection des données.
|
|
|
|
\paragraph{Pipeline} mise en pratique de la décomposition par data flow.
|
|
|
|
\paragraph{Wave front} mise en œuvre d'un tri topologique parallélisée. On
|
|
traite une tâche quand on a traitée ses prédécesseurs.
|
|
|
|
\paragraph{Geometric decomposition} façon de couper les blocs de données qui ne
|
|
sont pas forcément contigus.
|
|
|
|
|
|
\chapter{Interragir avec le cache du CPU}
|
|
|
|
Le cache est nécessaire du fait que la mémoire est beaucoup plus lente que le
|
|
processeur.
|
|
|
|
Le cache est géré par ligne~: de quelques dizaine d'octets. Chaque ligne a un
|
|
état par rapport à sa cohérence. Lorsqu'elle est invalide cela signifie qu'une
|
|
modification a été effectuée en mémoire ou dans un autre cache (d'un autre
|
|
processeur par exemple). En état partagé, il n'est pas possible d'écrire
|
|
dessus, on peut seulement la lire. En mode exclusif, seul un processeur peut
|
|
écrire la zone mémoire, elle est ensuite noté modified.
|
|
|
|
\section{False sharing}
|
|
|
|
Par moment, il peut y avoir deux processeurs qui utilisent deux valeurs
|
|
différentes mais dans des lignes de cache identiques. Du coup il va invalider
|
|
une ligne de cache sur l'autre processeur alors qu'en réalité, la valeur qui
|
|
intéressait le processeur n'a pas été modifiée. On parle de \emph{False
|
|
sharing}.
|
|
|
|
Pour éviter ce genre de chose, il faut utiliser le plus de variable locale
|
|
possible dans chaque thread, et avoir le moins de données communes.\\
|
|
|
|
\section{Memory fence}
|
|
|
|
|
|
\chapter{Exclusion mutuelle}
|
|
|
|
Une section critique est une section de code où l'on manipule des données
|
|
partagées (quand on dépend d'événement d'écriture sur une zone mémoire).
|
|
|
|
\section{Problème classique~: compteur partagé}
|
|
|
|
On a un compteur partagé entre deux threads.
|
|
|
|
|
|
\paragraph{Exclussion mutuelle~:} deux threads ne doivent pas pouvoir être au même endroit
|
|
en même temps.
|
|
|
|
\paragraph{Progression~:} lorsque l'on veut aller dans une section critique, on ne doit
|
|
attendre que parce que quelqu'un y est déjà.
|
|
|
|
\paragraph{Attente bornée~:} quand on attend la section critique. On attend un nombre fini
|
|
de thread.
|
|
|
|
\paragraph{Deadlock~:} mauvaise combinaison d'accès du coup deux thread s'attendent
|
|
mutuellement.
|
|
|
|
\paragraph{Race condition~:} sous certaines conditions d'éxécution, certaines propriétés ne
|
|
sont pas respectées.
|
|
|
|
\paragraph{Famine~:} un thread est en famine quand il attend indéfiniment une
|
|
ressources.
|
|
|
|
|
|
|
|
\chapter{Techniques de lock}
|
|
|
|
\section{Comment locker~?}
|
|
|
|
\subsection{Bloquer les interruptions et la mémoire}
|
|
|
|
\subsubsection{Interruptions}
|
|
|
|
Dans le code on déclenche des interruptions de temps en temps pour
|
|
changer de thread.
|
|
|
|
\subsubsection{Mémoire}
|
|
|
|
On bloque un espace mémoire le temps de faire une série d'opération.
|
|
|
|
|
|
\subsection{Test and set}
|
|
|
|
On swap 1 avec le contenu d'une adresse mémoire. Ensuite on lit le
|
|
contenu récupéré. S'il vaut 1, on continue de boucler. S'il vaut 0,
|
|
tout autre thread obtiendrait un 1 du fait que l'opération de swap est
|
|
atomique.
|
|
|
|
\subsection{Compare and swap}
|
|
|
|
On lui donne une adresse, une valeur de test, une nouvelle valeur.
|
|
|
|
Exemple~: $+=$, $-=$, \ldots
|
|
|
|
\subsection{Mutex}
|
|
|
|
C'est un verrou que l'on utilise comme valeur abstraite qui correspond
|
|
à une porte de toilette.
|
|
|
|
C'est implémenté à base de compare and swap.
|
|
\newline
|
|
|
|
Pthread n'utilise pas des mutex équitables, ni implémentés sous forme
|
|
de pile, ils sont réentrants et sont en attente passive.
|
|
|
|
\newline
|
|
|
|
L'attente passive c'est le fait de donner au kernel la charge de
|
|
réveiller le thread lorsqu'un autre syscall indiquant qu'il a fini
|
|
d'exécuter le code critique.
|
|
|
|
Du fait du nombre de syscall, il est intéressant de garder un spin
|
|
lock plutôt qu'un mutex.
|
|
|
|
\subsection{Barrière}
|
|
|
|
C'est un outil de synchronisation. Elle reste fermée tant qu'il n'y a
|
|
pas assez de monde pour rentrer. La barrière se referme derrière le
|
|
dernier.
|
|
|
|
Peut utilisé, sauf avec Cuda sur les cartes graphiques.
|
|
|
|
\subsection{Read/Write lock}
|
|
|
|
Répond au problème du lecteur/rédacteur.
|
|
|
|
Des thread lisent la zone mémoire et d'autre écrivent la zone
|
|
mémoire.
|
|
|
|
Lorsqu'il n'y a pas d'écriture, on peut lire sans gérer les
|
|
locks. Mais lorsqu'il y a de l'écriture, il peut arriver que l'on lise
|
|
le début de la réécriture puis l'ancien contenu écrit.
|
|
|
|
On a donc deux mutex~: un pour la lecture, un autre pour
|
|
l'écriture. Si le mutex d'écriture est actif, on attend systématique,
|
|
si le mutex de lecture est actif, on autorise les autres lecteure,
|
|
mais on attend pour faire les écritures.
|
|
|
|
\subsection{Variable de condition}
|
|
|
|
On associe une condition à un mutex. On boucle sur la condition, et on
|
|
wait (sans oublier de libérer le mutex) tant que la condition n'est
|
|
pas valide.
|
|
|
|
|
|
\subsection{Sémaphores}
|
|
|
|
On a un compteur protégé que l'on peut incrémenté ou
|
|
décrémenter. Quand ce compteur est supérieur ou égal à 0~: le
|
|
sémaphore est réveillé. Lorsqu'il est à 0 et qu'il est décrémenté, il
|
|
se met en sommeil.
|
|
|
|
Généralement, on utiliser les opérations P et V. P incrémente le
|
|
compteur et V le décrémente. Par défaut, on a une notion de pile.
|
|
|
|
\subsection{Moniteur}
|
|
|
|
L'avantage des moniteurs est de cacher l'utilisation des wait
|
|
Cela requiert d'avoir un langage objet.
|
|
|
|
\section{Le diner des philosophes}
|
|
|
|
On a 5 philosophes.
|
|
|
|
\section{Structure de données}
|
|
|
|
Les B-tree sont très adaptés au parallélisme.
|
|
|
|
Les listes doublement chaînées sont à l'inverse, le pire choix
|
|
possible.
|
|
|
|
\subsection{Structures non bloquantes}
|
|
|