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. 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.

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 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.

Herb Sutter a écrit quelques GOTW (Guru of the Week) en 2014 sur le sujet des conditions de course et de la synchronisation. Voir :

Lectures complémentaires

Quelques liens suivent pour enrichir le propos.

À propos des mutex et de leurs équivalents conceptuels :


Valid XHTML 1.0 Transitional

CSS Valide !