Lock, mutex, semaphore… quelle est la différence?


Réponses:


534

Un verrou permet à un seul thread d'entrer dans la partie verrouillée et le verrou n'est partagé avec aucun autre processus.

Un mutex est identique à un verrou mais il peut être à l'échelle du système (partagé par plusieurs processus).

Un sémaphore fait la même chose qu'un mutex mais permet à x nombre de threads d'entrer, ceci peut être utilisé par exemple pour limiter le nombre de tâches gourmandes en CPU, en io ou en RAM s'exécutant en même temps.

Pour un article plus détaillé sur les différences entre mutex et sémaphore, lisez ici .

Vous avez également des verrous en lecture / écriture qui permettent à tout moment un nombre illimité de lecteurs ou 1 écrivain.


2
@mertinan je ne peux pas dire que j'en ai jamais entendu parler, mais c'est ce que wikipedia dit "Latch (base de données), (un verrouillage relativement court) sur une structure de données système comme un index"
Peter

2
Le moniteur permet d'attendre une certaine condition (par exemple lorsque le verrou est libéré), "surveille".
Dzmitry Lazerka

25
Un sémaphore n'est pas la même chose qu'un mutex. Ils sont utilisés très différemment et ont également des propriétés différentes (notamment en ce qui concerne la propriété). Voir par exemple barrgroup.com/Embedded-Systems/How-To/RTOS-Mutex-Semaphore pour plus de détails
nanoquack

3
@nanoquack n'hésitez pas à modifier ma réponse si vous pensez qu'elle est trompeuse ou incorrecte.
Peter

3
Pour une distinction plus claire entre mutex et sémaphore, dans le lien de nanoquack, le paragraphe clé est " L'utilisation correcte d'un sémaphore est pour signaler d'une tâche à une autre. Un mutex est destiné à être pris et libéré, toujours dans cet ordre, par chacun tâche qui utilise la ressource partagée qu'elle protège. En revanche, les tâches qui utilisent des sémaphores signalent ou attendent, mais pas les deux. "
ToolmakerSteve

117

Il y a beaucoup d'idées fausses concernant ces mots.

Ceci provient d'un article précédent ( https://stackoverflow.com/a/24582076/3163691 ) qui convient parfaitement ici:

1) Section critique = objet utilisateur utilisé pour permettre l'exécution d'un seul thread actif à partir de nombreux autres dans un même processus . Les autres threads non sélectionnés (@ acquisition de cet objet) sont mis en veille .

[Pas de capacité interprocessus, objet très primitif].

2) Mutex Semaphore (aka Mutex) = Objet noyau utilisé pour permettre l'exécution d'un seul thread actif parmi de nombreux autres, parmi différents processus . Les autres threads non sélectionnés (@ acquisition de cet objet) sont mis en veille . Cet objet prend en charge la propriété des threads, la notification de terminaison des threads, la récursivité (plusieurs appels «d'acquisition» à partir du même thread) et «l'évitement d'inversion de priorité».

[Capacité interprocessus, très sûre à utiliser, une sorte d'objet de synchronisation «de haut niveau»].

3) Counting Semaphore (aka Semaphore) = Objet noyau utilisé pour permettre l'exécution d' un groupe de threads actifs à partir de nombreux autres. Les autres threads non sélectionnés (@ acquisition de cet objet) sont mis en veille .

[La capacité interprocessus n'est cependant pas très sûre à utiliser car elle n'a pas les attributs «mutex» suivants: notification de terminaison de thread, récursivité?, «Évitement d'inversion de priorité»?, Etc.].

4) Et maintenant, en parlant de «spinlocks», d'abord quelques définitions:

Région critique = Région de mémoire partagée par 2 processus ou plus.

Lock = Une variable dont la valeur autorise ou refuse l'entrée d'une «région critique». (Il pourrait être implémenté comme un simple «drapeau booléen»).

Attente occupée = Test continu d'une variable jusqu'à ce qu'une certaine valeur apparaisse.

Finalement:

Spin-lock (aka Spinlock) = Un verrou qui utilise l' attente occupée . (L'acquisition du verrou se fait par xchg ou des opérations atomiques similaires ).

[Aucun thread en veille, principalement utilisé au niveau du noyau uniquement. Ineffcient pour le code de niveau utilisateur].

En dernier commentaire, je ne suis pas sûr mais je peux vous parier quelques gros dollars que les 3 premiers objets de synchronisation ci-dessus (# 1, # 2 et # 3) utilisent cette bête simple (# 4) dans le cadre de leur mise en œuvre.

Bonne journée!.

Références:

-Concepts en temps réel pour les systèmes embarqués par Qing Li avec Caroline Yao (CMP Books).

-Systèmes d'exploitation modernes (3e) par Andrew Tanenbaum (Pearson Education International).

-Programmation d'applications pour Microsoft Windows (4e) par Jeffrey Richter (Microsoft Programming Series).

En outre, vous pouvez jeter un œil à l' adresse suivante : https://stackoverflow.com/a/24586803/3163691


1
En fait, la section critique n'est pas un objet du noyau, donc plus légère et incapable de se synchroniser entre les processus.
Vladislavs Burakovs

2
@ Vladislavs Burakovs: Vous avez raison! Pardonnez ma rédaction. Je vais le réparer pour des raisons de cohérence.
fante

Pour une distinction plus claire entre mutex et sémaphore, comme le mentionne nanoquack ailleurs, voir barrgroup.com/Embedded-Systems/How-To/RTOS-Mutex-Semaphore - Le paragraphe clé est " L'utilisation correcte d'un sémaphore est pour la signalisation d'une tâche à un autre. Un mutex est censé être pris et libéré, toujours dans cet ordre, par chaque tâche qui utilise la ressource partagée qu'il protège. En revanche, les tâches qui utilisent des sémaphores signalent ou attendent, mais pas les deux. "
ToolmakerSteve

Re conjecture que d'autres mécanismes de verrouillage s'appuient sur un verrou tournant [inefficace]: peu probable; L'AFAIK n'a besoin que de quelques opérations atomiques et de files d'attente. Même lorsqu'un spinlock est nécessaire à l'intérieur du noyau, les solutions modernes minimisent son impact comme décrit dans Wikipedia - Spinlock - Alternatives - " .. utiliser une approche hybride appelée" mutex adaptatif ". L'idée est d'utiliser un spinlock lorsque vous essayez d'accéder à une ressource verrouillée par un thread en cours d'exécution, mais de dormir si le thread n'est pas en cours d'exécution. (Ce dernier est toujours le cas sur les systèmes à processeur unique.) "
ToolmakerSteve

@ToolmakerSteve, je vous mets au défi de fournir une «solution» sans «verrou tournant» pour le problème des «collisions» en essayant «d'insérer» un ID de fil dans une «file d'attente». Quoi qu'il en soit, le texte de Wikipédia conclut qu'un verrou tournant est utilisé lors de la mise en œuvre !!!.
fante

27

La plupart des problèmes peuvent être résolus en utilisant (i) uniquement des verrous, (ii) uniquement des sémaphores, ..., ou (iii) une combinaison des deux! Comme vous l'avez peut-être découvert, ils sont très similaires: les deux empêchent les conditions de concurrence , les deux ont acquire()/ release()opérations, les deux provoquent le blocage / soupçon de zéro ou plusieurs threads ... Vraiment, la différence cruciale réside uniquement dans la façon dont ils se verrouillent et se déverrouillent .

  • Un verrou (ou mutex ) a deux états (0 ou 1). Il peut être déverrouillé ou verrouillé . Ils sont souvent utilisés pour garantir qu'un seul thread entre dans une section critique à la fois.
  • Un sémaphore a plusieurs états (0, 1, 2, ...). Il peut être verrouillé (état 0) ou déverrouillé (états 1, 2, 3, ...). Un ou plusieurs sémaphores sont souvent utilisés ensemble pour garantir qu'un seul thread entre dans une section critique précisément lorsque le nombre d'unités d'une ressource a / n'a pas atteint une valeur particulière (soit en comptant jusqu'à cette valeur, soit en comptant jusqu'à cette valeur) ).

Pour les deux verrous / sémaphores, essayer d'appeler acquire()alors que la primitive est à l'état 0 entraîne la suspension du thread appelant . Pour les verrous - les tentatives pour acquérir le verrou dans l'état 1 ont réussi. Pour les sémaphores - les tentatives d'acquisition du verrou dans les états {1, 2, 3, ...} ont réussi.

Pour les verrous à l'état 0, si le même thread qui avait précédemment appelé acquire()appelle maintenant la libération, la libération réussit. Si un thread différent a essayé cela - c'est à l'implémentation / bibliothèque de savoir ce qui se passe (généralement la tentative est ignorée ou une erreur est levée). Pour les sémaphores à l'état 0, n'importe quel thread peut appeler release et cela réussira (quel que soit le thread utilisé précédemment pour acquérir le sémaphore à l'état 0).

De la discussion précédente, nous pouvons voir que les verrous ont une notion de propriétaire (le seul thread qui peut appeler release est le propriétaire), tandis que les sémaphores n'ont pas de propriétaire (n'importe quel thread peut appeler release sur un sémaphore).


Ce qui cause beaucoup de confusion, c'est que, dans la pratique, il existe de nombreuses variantes de cette définition de haut niveau.

Variations importantes à considérer :

  • Quel devrait être le acquire()/ release()être appelé? - [Varie massivement ]
  • Votre verrou / sémaphore utilise-t-il une "file d'attente" ou un "ensemble" pour se souvenir des threads en attente?
  • Votre verrou / sémaphore peut-il être partagé avec des threads d'autres processus?
  • Votre serrure est-elle "réentrante"? - [Généralement oui].
  • Votre serrure est-elle "bloquante / non bloquante"? - [Normalement, le non-blocage est utilisé car les verrous de blocage (aka spin-locks) provoquent une attente occupée].
  • Comment vous assurez-vous que les opérations sont "atomiques"?

Cela dépend de votre livre / professeur / langue / bibliothèque / environnement.
Voici un rapide tour d'horizon notant comment certaines langues répondent à ces détails.


C, C ++ ( pthreads )

  • Un mutex est implémenté via pthread_mutex_t. Par défaut, ils ne peuvent pas être partagés avec d'autres processus ( PTHREAD_PROCESS_PRIVATE), cependant les mutex ont un attribut appelé pshared . Lorsqu'il est défini, le mutex est partagé entre les processus ( PTHREAD_PROCESS_SHARED).
  • Un verrou est la même chose qu'un mutex.
  • Un sémaphore est implémenté via sem_t. Semblable aux mutex, les sémaphores peuvent être partagés entre des menaces de nombreux processus ou gardés privés pour les threads d'un seul processus. Cela dépend de l' argument pshared fourni à sem_init.

python ( threading.py )

  • Un verrou ( threading.RLock) est essentiellement le même que C / C ++ pthread_mutex_t. Les deux sont tous deux réentrants . Cela signifie qu'ils ne peuvent être déverrouillés que par le même fil qui l'a verrouillé. Il est vrai que les sem_tsémaphores, les threading.Semaphoresémaphores et les theading.Lockverrous ne sont pas réentrants - car c'est le cas où n'importe quel thread peut effectuer un déverrouillage du verrouillage / descente du sémaphore.
  • Un mutex est identique à un verrou (le terme n'est pas souvent utilisé en python).
  • Un sémaphore ( threading.Semaphore) est essentiellement le même que sem_t. Bien qu'avec sem_t, une file d'attente d'ID de threads est utilisée pour se souvenir de l'ordre dans lequel les threads ont été bloqués lors de la tentative de verrouillage pendant qu'il est verrouillé. Lorsqu'un thread déverrouille un sémaphore, le premier thread de la file d'attente (s'il y en a un) est choisi pour être le nouveau propriétaire. L'identificateur de thread est retiré de la file d'attente et le sémaphore est à nouveau verrouillé. Cependant, avec threading.Semaphore, un ensemble est utilisé à la place d'une file d'attente, donc l'ordre dans lequel les threads ont été bloqués n'est pas stocké - n'importe quel thread de l'ensemble peut être choisi pour être le prochain propriétaire.

Java ( java.util.concurrent )

  • Un verrou ( java.util.concurrent.ReentrantLock) est essentiellement le même que celui de C / C ++ pthread_mutex_tet de Python threading.RLocken ce qu'il implémente également un verrou réentrant. Le partage de verrous entre les processus est plus difficile en Java car la JVM agit comme un intermédiaire. Si un thread essaie de déverrouiller un verrou qu'il ne possède pas, un IllegalMonitorStateExceptionest levé.
  • Un mutex est identique à un verrou (le terme n'est pas souvent utilisé en Java).
  • Un sémaphore ( java.util.concurrent.Semaphore) est essentiellement le même que sem_tet threading.Semaphore. Le constructeur des sémaphores Java accepte un paramètre booléen d' équité qui contrôle s'il faut utiliser un ensemble (faux) ou une file d'attente (vrai) pour stocker les threads en attente.

En théorie, les sémaphores sont souvent discutés, mais en pratique, les sémaphores ne sont pas tellement utilisés. Un sémaphore ne contient que l'état d' un entier, si souvent il est plutôt rigide et plusieurs sont nécessaires à la fois, ce qui rend difficile la compréhension du code. De plus, le fait que n'importe quel thread puisse libérer un sémaphore est parfois indésirable. Des primitives / abstractions de synchronisation plus orientées objet / de niveau supérieur telles que des "variables de condition" et des "moniteurs" sont utilisées à la place.


22

Jetez un oeil à Multithreading Tutorial par John Kopplin.

Dans la section Synchronisation entre les threads , il explique les différences entre les événements, les verrous, les mutex, les sémaphores et les temporisations.

Un mutex ne peut appartenir qu'à un seul thread à la fois, ce qui permet aux threads de coordonner l'accès mutuellement exclusif à une ressource partagée

Les objets de section critiques fournissent une synchronisation similaire à celle fournie par les objets mutex, sauf que les objets de section critiques ne peuvent être utilisés que par les threads d'un seul processus

Une autre différence entre un mutex et une section critique est que si l'objet de section critique appartient actuellement à un autre thread, EnterCriticalSection()attend indéfiniment la propriété tandis que WaitForSingleObject(), qui est utilisé avec un mutex, vous permet de spécifier un délai d'expiration

Un sémaphore conserve un nombre compris entre zéro et une valeur maximale, limitant le nombre de threads qui accèdent simultanément à une ressource partagée.


15

Je vais essayer de le couvrir d'exemples:

Verrouiller: Un exemple où vous utiliseriez lockserait un dictionnaire partagé dans lequel des éléments (qui doivent avoir des clés uniques) sont ajoutés.
Le verrou garantirait qu'un thread n'entre pas dans le mécanisme de code qui vérifie que l'élément est dans le dictionnaire tandis qu'un autre thread (qui se trouve dans la section critique) a déjà passé cette vérification et ajoute l'élément. Si un autre thread essaie d'entrer un code verrouillé, il attendra (sera bloqué) jusqu'à ce que l'objet soit libéré.

private static readonly Object obj = new Object();

lock (obj) //after object is locked no thread can come in and insert item into dictionary on a different thread right before other thread passed the check...
{
    if (!sharedDict.ContainsKey(key))
    {
        sharedDict.Add(item);
    }
}

Sémaphore: Disons que vous avez un pool de connexions, alors un seul thread peut réserver un élément dans le pool en attendant que le sémaphore obtienne une connexion. Il utilise ensuite la connexion et lorsque le travail est terminé, libère la connexion en libérant le sémaphore.

L'exemple de code que j'aime est celui d'un videur donné par @Patric - le voici:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace TheNightclub
{
    public class Program
    {
        public static Semaphore Bouncer { get; set; }

        public static void Main(string[] args)
        {
            // Create the semaphore with 3 slots, where 3 are available.
            Bouncer = new Semaphore(3, 3);

            // Open the nightclub.
            OpenNightclub();
        }

        public static void OpenNightclub()
        {
            for (int i = 1; i <= 50; i++)
            {
                // Let each guest enter on an own thread.
                Thread thread = new Thread(new ParameterizedThreadStart(Guest));
                thread.Start(i);
            }
        }

        public static void Guest(object args)
        {
            // Wait to enter the nightclub (a semaphore to be released).
            Console.WriteLine("Guest {0} is waiting to entering nightclub.", args);
            Bouncer.WaitOne();          

            // Do some dancing.
            Console.WriteLine("Guest {0} is doing some dancing.", args);
            Thread.Sleep(500);

            // Let one guest out (release one semaphore).
            Console.WriteLine("Guest {0} is leaving the nightclub.", args);
            Bouncer.Release(1);
        }
    }
}

Mutex Il est à peu près Semaphore(1,1)et souvent utilisé à l'échelle mondiale (à l'échelle de l'application sinon, il lockest sans doute plus approprié). On utiliserait global Mutexlors de la suppression du nœud d'une liste accessible globalement (dernière chose que vous voulez qu'un autre thread fasse quelque chose pendant que vous supprimez le nœud). Lorsque vous acquérez Mutexsi un thread différent essaie d'acquérir le même, Mutexil sera mis en veille jusqu'à ce que le même thread qui l'a acquis le Mutexlibère.

Un bon exemple sur la création d'un mutex mondial est de @deepee

class SingleGlobalInstance : IDisposable
{
    public bool hasHandle = false;
    Mutex mutex;

    private void InitMutex()
    {
        string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();
        string mutexId = string.Format("Global\\{{{0}}}", appGuid);
        mutex = new Mutex(false, mutexId);

        var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
        var securitySettings = new MutexSecurity();
        securitySettings.AddAccessRule(allowEveryoneRule);
        mutex.SetAccessControl(securitySettings);
    }

    public SingleGlobalInstance(int timeOut)
    {
        InitMutex();
        try
        {
            if(timeOut < 0)
                hasHandle = mutex.WaitOne(Timeout.Infinite, false);
            else
                hasHandle = mutex.WaitOne(timeOut, false);

            if (hasHandle == false)
                throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance");
        }
        catch (AbandonedMutexException)
        {
            hasHandle = true;
        }
    }


    public void Dispose()
    {
        if (mutex != null)
        {
            if (hasHandle)
                mutex.ReleaseMutex();
            mutex.Dispose();
        }
    }
}

puis utilisez comme:

using (new SingleGlobalInstance(1000)) //1000ms timeout on global lock
{
    //Only 1 of these runs at a time
    GlobalNodeList.Remove(node)
}

J'espère que cela vous fera gagner du temps.


8

Wikipedia a une grande section sur les différences entre les sémaphores et les mutex :

Un mutex est essentiellement la même chose qu'un sémaphore binaire et utilise parfois la même implémentation de base. Les différences entre eux sont:

Les mutex ont un concept de propriétaire, qui est le processus qui a verrouillé le mutex. Seul le processus qui a verrouillé le mutex peut le déverrouiller. En revanche, un sémaphore n'a aucun concept de propriétaire. Tout processus peut déverrouiller un sémaphore.

Contrairement aux sémaphores, les mutex offrent une sécurité d'inversion prioritaire. Étant donné que le mutex connaît son propriétaire actuel, il est possible de promouvoir la priorité du propriétaire chaque fois qu'une tâche de priorité supérieure commence à attendre sur le mutex.

Les mutex offrent également une sécurité de suppression, où le processus contenant le mutex ne peut pas être accidentellement supprimé. Les sémaphores ne fournissent pas cela.


5

Ma compréhension est qu'un mutex est uniquement destiné à être utilisé dans un seul processus, mais sur ses nombreux threads, alors qu'un sémaphore peut être utilisé sur plusieurs processus et sur leurs ensembles de threads correspondants.

En outre, un mutex est binaire (il est verrouillé ou déverrouillé), tandis qu'un sémaphore a une notion de comptage ou une file d'attente de plusieurs demandes de verrouillage et déverrouillage.

Quelqu'un pourrait-il vérifier mon explication? Je parle dans le contexte de Linux, en particulier de Red Hat Enterprise Linux (RHEL) version 6, qui utilise le noyau 2.6.32.


3
Maintenant, cela peut être différent dans différents systèmes d'exploitation, mais dans Windows, un Mutex peut être utilisé par plusieurs processus au moins l'objet Mutex .net.
Peter

2
stackoverflow.com/questions/9389730/… "Les threads d'un même processus ou d'autres processus peuvent partager des mutex." donc aucun mutex ne doit être spécifique au processus.
Peter

3

Utilisation de la programmation C sur une variante Linux comme cas de base pour des exemples.

Fermer à clé:

• Habituellement, un binaire de construction très simple en fonctionnement verrouillé ou déverrouillé

• Aucun concept de propriété de thread, de priorité, de séquencement, etc.

• Habituellement, un verrou tournant où le fil vérifie en permanence la disponibilité des verrous.

• Se fonde généralement sur des opérations atomiques, par exemple, tester et définir, comparer et échanger, extraire et ajouter, etc.

• Nécessite généralement un support matériel pour le fonctionnement atomique.

Verrous de fichiers:

• Habituellement utilisé pour coordonner l'accès à un fichier via plusieurs processus.

• Plusieurs processus peuvent contenir le verrou de lecture. Cependant, lorsqu'un seul processus détient le verrou d'écriture, aucun autre processus n'est autorisé à acquérir un verrou de lecture ou d'écriture.

• Exemple: flock, fcntl etc.

Mutex:

• Les appels de fonction Mutex fonctionnent généralement dans l'espace du noyau et entraînent des appels système.

• Il utilise le concept de propriété. Seul le thread qui détient actuellement le mutex peut le déverrouiller.

• Mutex n'est pas récursif (exception: PTHREAD_MUTEX_RECURSIVE).

• Habituellement utilisé en association avec les variables de condition et transmis comme arguments à pthread_cond_signal, pthread_cond_wait, etc.

• Certains systèmes UNIX permettent à mutex d'être utilisé par plusieurs processus bien que cela ne soit pas forcément appliqué sur tous les systèmes.

Sémaphore:

• Il s'agit d'un entier maintenu par le noyau dont les valeurs ne doivent pas tomber en dessous de zéro.

• Il peut être utilisé pour synchroniser les processus.

• La valeur du sémaphore peut être définie sur une valeur supérieure à 1, auquel cas la valeur indique généralement le nombre de ressources disponibles.

• Un sémaphore dont la valeur est limitée à 1 et 0 est appelé sémaphore binaire.


0

Supporting ownership, maximum number of processes share lockEt maximum number of allowed processes/threads in critical sectionsont trois principaux facteurs qui déterminent le nom / type de l'objet en même temps que le nom général de lock. Étant donné que la valeur de ces facteurs est binaire (a deux états), nous pouvons les résumer dans un tableau de vérité 3 * 8.

  • X (Prise en charge de la propriété?): Non (0) / oui (1)
  • Y (# processus de partage):> 1 (∞) / 1
  • Z (# processus / threads dans CA):> 1 (∞) / 1

  X   Y   Z          Name
 --- --- --- ------------------------
  0   ∞   ∞   Semaphore              
  0   ∞   1   Binary Semaphore       
  0   1   ∞   SemaphoreSlim          
  0   1   1   Binary SemaphoreSlim(?)
  1   ∞   ∞   Recursive-Mutex(?)     
  1   ∞   1   Mutex                  
  1   1   ∞   N/A(?)                 
  1   1   1   Lock/Monitor           

N'hésitez pas à éditer ou développer ce tableau, je l'ai posté en tant que tableau ascii pour être éditable :)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.