Synchronisation avec verrous
« "Mutexes" should have been called "bottlenecks" to make it more obvious what they actually do » (Kevlin Henney)
Pour des ressources plus générales sur la
synchronisation, voir Synchronisation.html
Cette page sera enrichie pour discuter de mutex récursifs, de sémaphores,
de multiple-readers single-writer locks, etc.
La technique la plus simple pour synchroniser l'accès à des états d'un
programme est de recourir à des verrous.
Mutex
Le verrou le plus simple sur le plan
conceptuel est le mutex, pour Mutual Exclusion, qui représente un
droit d'accès exclusif à une ressource :
Avec
C++ |
Avec
Java |
Avec
C# |
#include <thread>
#include <mutex>
using namespace std;
int main() {
mutex m;
auto th0 = thread {
[&]() {
lock_guard<mutex> _ { m };
cout << "Je suis "
<< "le thread "
<< "zero" << endl;
}
};
auto th1 = thread {
[&]() {
lock_guard<mutex> _ { m };
cout << "Je suis "
<< "le thread "
<< "un" << endl;
}
};
th1.join();
th0.join();
}
|
public class Test {
static class Thr extends Thread {
Object m;
String id;
public Thr(Object m, String id) {
this.m = m;
this.id = id;
}
public void run() {
synchronized(m) {
System.out.print("Je suis ");
System.out.print("le thread ");
System.out.println(id);
}
}
}
public static void main(String [] args) {
Object mutex = new Object();
Thr th0 = new Thr(mutex, "zero");
Thr th1 = new Thr(mutex, "un");
th0.start();
th1.start();
}
}
|
using System;
using System.Threading;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Mutex m = new Mutex();
var th0 = new Thread(() =>
{
lock (m)
{
Console.Write("Je suis ");
Console.Write("le thread ");
Console.WriteLine("zero");
}
});
var th1 = new Thread(() =>
{
lock (m)
{
Console.Write("Je suis ");
Console.Write("le thread ");
Console.WriteLine("un");
}
});
th0.Start();
th1.Start();
th1.Join();
th0.Join();
}
}
}
|
Ces trois exemples font la même chose, soit synchroniser une séquence
d'écriture à la sortie standard pour éviter un entremêlement des messages.
Le zoo des mutex
Les mutex de
C++ forment un petit zoo d'options pour qui souhaite
synchroniser son code concurrent à l'aide de verrous :
Les versions _shared permettent à plusieurs fils d'exécution de
verrouiller un même mutex, typiquement pour permettre la lecture
concurrente.
Les versions _timed offrent des mécanismes pour tenter de verrouiller un
mutex un certain temps, puis d'abandonner s'ils n'y parviennent pas.
Habituellement, pour une plateforme donnée, les mécanismes les moins
« puissants » peuvent être implémentés plus efficacement que les
mécanismes les plus « puissants », et sont aussi un peu plus
légers. Pour cette raison, les programmeuses et les programmeurs auront
tendance à utiliser le mécanisme le moins « puissant » qui suffise à
leurs besoins.
Pour faciliter le verrouillage et automatiser le déverrouilage, on aura
typiquement recours aux mécanismes suivants (notez que j'utilise
CTAD de
C++ 17
pour alléger l'écriture), tous incopiables et tous
RAII :
lock_guard<T> |
Type dont le constructeur verrouille un mutex et un destructeur le
déverrouille. Une implémentation maison simple et naïve (en pratique,
std::lock_guard offre d'autres modalités de prise en charge d'un
mutex que celle-ci) serait :
template <class M>
class lock_guard {
M &m;
public:
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
lock_guard(M &m) : m{ m } { m.lock(); }
~lock_guard() { m.unlock(); }
};
|
// ...
mutex m;
// ...
{
lock_guard _{ m }; // m.lock()
// ...
} // m.unlock()
|
unique_lock<T> |
Un unique_lock est seul responsable du
mutex, mais offre une gamme de services bien plus complexe que ne le fait
un lock_guard : il peut être testé pour savoir
s'il possède ou non un mutex, il peut le déverrouiller, le verrouiller à
nouveau ultérieurement, etc.
|
// ...
mutex m;
// ...
{
unique_lock _{ m }; // m.lock()
// ...
} // m.unlock()
|
scoped_lock<Ts...> |
Un scoped_lock est un peu comme un
lock_guard mais est capable de prendre en charge un nombre
variadique de mutex.
|
// ...
mutex m0, m1, m2;
// ...
{
scoped_lock _{ m0, m1, m2 }; // lock() dans un ordre
// fixé et indépendant de
// l'ordre des paramètre
// ...
} // unlock() en ordre inverse des lock()
|
shared_lock<T> |
Un shared_lock permet de réaliser des cas plus subtils comme le
verrouillage pour plusieurs lecteurs et un seul scripteur
|
Voir Verrous-SWMR.html Pour des exemples |
Sémaphores
Depuis
C++ 20, le langage
C++ offre un
en-tête
<semaphore>
exposant deux types de sémaphores. J'ajouterai des exemples ici dès que j'En aurai le temps.
Un autre type de verrou est le sémaphore, qui généralise le concept de verrou en donnant un accès
à demandeurs à une même ressource. Un mutex est au fond un sémaphore pour
lequel .
Le sémaphorem tout comme le mutex, peut être implémenté à même le
système d'exploitation,
ce qui en fait alors un outil puissant mais lourd (chaque accès est alors un
appel système). Pour synchroniser des accès entre
threads à l'intérieur d'un même
processus, il existe des variantes plus légères, comme les sections critiques
sous Microsoft
Windows et les
futex, pour Fast Userspace Mutex,
sous Linux. Notes
toutefois que les
futex
servent principalement à attendre efficacement le passage d'une variable
atomique à un état particulier,
et sont donc de portée plus limitée que les mutex.
Les implémentations commerciales et standards de mutex
offrent typiquement des fonctions de base telles lock()
(bloquer jusqu'à obtention de la ressource),
try_lock() (tenter d'obtenir la ressource et retourner un code de succès
ou d'erreur) avec variantes telles que try_lock_for()
et try_lock_until() pour prendre en charge
les timeouts, et unlock() (libérer un
mutex préalablement acquis).
Notez que typiquement, un try_lock... peut
échouer de manière intempestive, mais ne retournera que des faux négatifs
(retourner un code d'échec même si le mutex était disponible), jamais de faux
positifs (heureusement!); le code client doit être écrit en conséquence.
Le Global Interpreter Lock, ou GIL
Une solution simple, mais terriblement inefficace, pour la synchronisation
à travers des verrous est d'utiliser un seul verrou global et de transformer
un programme multiprogrammé en programme monoprogrammé lorsque des risques de
concurrence surviennent. Quelques langages procèdent ainsi, en particulier
Python et
Ruby.
Le recours à une mémoire
transactionnelle donne l'illusion d'un GIL,
sans toutefois reposer sur un tel mécanisme.
Lectures complémentaires
Quelques liens suivent pour enrichir le propos.
- Les verrous pour lectures concurrentes mais écriture unique, ou Readers-Writer
Locks (remarquez les pluriels et les singuliers) : http://en.wikipedia.org/wiki/Readers-writer_lock
- À propos des sémantiques d'acquisition et de libération
sous Microsoft
Windows, voir http://msdn.microsoft.com/en-us/library/aa490209.aspx
- De manière générale, un résumé des outils
de synchronisation :
- En 2011,
Anthony
Williams nous rappelle qu'encapsuler nos verrous améliore notre code : http://www.justsoftwaresolutions.co.uk/threading/simplify_code_by_encapsulating_locks.html
- Concevoir des verrous à partir d'opérations
atomiques,
un texte de Steven Fuerst :
http://locklessinc.com/articles/locks/
- À propos du choix d'une primitive de synchronisation, un texte d'Andrew
Binstock en 2008 :
http://software.intel.com/en-us/articles/choosing-between-synchronization-primitives/
- En 2011,
Jeff Preshing nous rappelle que ce ne
sont pas les verrous qui sont lents; c'est la
contention pour les verrous :
http://preshing.com/20111118/locks-arent-slow-lock-contention-is
- En 2011,
Jeff Preshing indique qu'il est essentiellement
toujours préférable d'utiliser la version légère
d'un outil de synchronisation :
http://preshing.com/20111124/always-use-a-lightweight-mutex
- Des verrous sur les fichiers : http://en.wikipedia.org/wiki/File_lock
- Les verrous dans
WebKit, selon Filip Pizlo en
2016 :
https://webkit.org/blog/6161/locking-in-webkit/
- Les verrous dans le
noyau de
Linux, présentation
de Davidlohr Bueso et Scott Norton en 2014 :
http://events.linuxfoundation.org/sites/events/files/slides/linuxcon-2014-locking-final.pdf
À propos des mutex et de leurs équivalents conceptuels :
- Mutex : http://en.wikipedia.org/wiki/Mutex
- Mutex réentrants : http://en.wikipedia.org/wiki/Reentrant_mutex
- Sémaphores :
- ../Client-Serveur/Mutex-Autoverrou.html
- ../Client-Serveur/Mutex-Portables.html
- À propos du fonctionnement d'un mutex et des coûts associés
à son utilisation, un texte d'Edaqa Mortoray en 2011 :
http://mortoray.com/2011/12/16/how-does-a-mutex-work-what-does-it-cost/
- Démystifier les mutex et les sémaphores, par Michael Barr en
2010 (perspective de systèmes embarqués) : http://www.netrino.com/node/202
- Comparatif de plusieurs implémentations possibles d'un mutex, par
Charles Bloom en 2011 :
http://cbloomrants.blogspot.ca/2011/07/07-15-11-review-of-many-mutex.html
- Pourquoi a-t-on besoin de mutex? http://eversystems.eu/Document/27/CPU_Memory_-_Why_do_I_need_a_mutex
- Optimiser le comportement des mutex dans certaines circonstances, texte de
Sanjoy Das en 2013 :
http://playingwithpointers.com/biased-locking-and-pthreads.html
- Réflexion critique sur les mutex récursifs, Par David Butenhof en
2005 :
http://zaval.org/resources/library/butenhof1.html
- Des verrous testables, pour réaliser un Try-Lock :
../Client-Serveur/Verrous-testables.html
- Une section critique, équivalent
Microsoft Windows
des futex de
Linux :
../Client-Serveur/Sections-Critiques-Portables.html
- Aller au-delà des sections critiques, présentation de 2008 par Tony Albrecht : http://seven-degrees-of-freedom.blogspot.com/2008/12/beyond-critical-section.html
- Une zone de transit :
../Client-Serveur/zone_transit.html
- Wiki sur les futex :
http://en.wikipedia.org/wiki/Futex
- À propos des subtilités associées aux futex,
texte d'Ulrich Drepper en 2011 :
http://www.akkadia.org/drepper/futex.pdf
- Choisir entre un mutex et un objet actif, par
Herb
Sutter en 2010 : http://www.drdobbs.com/go-parallel/article/showArticle.jhtml?articleID=227500074
- Textes pédagogiques de
Jeff Preshing sur la conception de mutex :
- Les sections critiques de
Microsoft
Windows sont différentes ce celles proposées à l'époque
pour OS/2, un texte de
2005 par Larry Osterman :
http://blogs.msdn.com/b/larryosterman/archive/2005/02/22/378174.aspx
- Pourquoi les sections critiques ne permettent-ellles pas une synchronisation
entre deux processus? Texte de Larry Osterman en 2005 :
http://blogs.msdn.com/b/larryosterman/archive/2005/08/24/455741.aspx
- En 2011,
Raymond Chen nous rappelle que si nous
protégeons une écriture sur un objet par voie de section critique,
il vaut sans doute mieux protéger aussi la lecture sur le même
objet :
http://blogs.msdn.com/b/oldnewthing/archive/2011/11/30/10242630.aspx
- Les sections critiques et la loi d'Amdahl, texte de 2012
par Andrew Trumper :
http://andrewtrumper.blogspot.ca/2012/01/amdahls-law-and-critical-sections.html
- Le GIL de
Python,
expliqué en 2009 :
http://www.dabeaz.com/python/GIL.pdf
- L'impact des verrous dans le monde
.NET :
- Synchronisation légère avec
Java
(texte de 1998) :
http://www.research.ibm.com/people/d/dfb/papers/Bacon98Thin.pdf
- Sans éliminer les verrous, certains cherchent à réduire
le recours à des verrous dans du code pris en charge par du verrouillage
biaisé :
- Les thread gates, analogue programmé des feux de circulation,
décrits dans une optique
Java
par Obi Ezechukwu en 2009 :
http://www.javaworld.com/javaworld/jw-03-2009/jw-03-java-concurrency-with-thread-gates.html
- En 2014,
Raymond Chen
explique ce qu'il advient d'un mutex lorsqu'il est en possession d'un
processus et que ce
dernier plante :
https://blogs.msdn.microsoft.com/oldnewthing/20140925-00/?p=43983/
- Texte d'Anthony
Williams en 2014 sur les diverses principales
variantes sur le thème du verrou :
https://www.justsoftwaresolutions.co.uk/threading/locks-mutexes-semaphores.html
- En 2015,
Jeff Preshing
discute de la polyvalence des sémaphores :
http://preshing.com/20150316/semaphores-are-surprisingly-versatile/
- Il semble qu'en 2016, l'implémentation des
mutex que propose Microsoft soit plus lente qu'espéré, du moins de l'avis de
Stoyan Nikolov :
https://stoyannk.wordpress.com/2016/04/30/msvc-mutex-is-slower-than-you-might-expect/
- Devrait-on faire l'acquisition d'un Slim Reader/Writer Lock (SRWLOCK)
récursivement? En 2016,
Raymond Chen
explique pourquoi ce n'est pas l'idée du siècle :
https://blogs.msdn.microsoft.com/oldnewthing/20160506-00/?p=93416