Exemple / tutoriel Mutex? [fermé]

176

Je suis nouveau dans le multithreading et j'essayais de comprendre le fonctionnement des mutex. J'ai fait beaucoup de recherches sur Google, mais cela a laissé des doutes sur son fonctionnement, car j'ai créé mon propre programme dans lequel le verrouillage ne fonctionnait pas.

Une syntaxe absolument non intuitive du mutex est pthread_mutex_lock( &mutex1 );, où il semble que le mutex est verrouillé, lorsque ce que je veux vraiment verrouiller est une autre variable. Cette syntaxe signifie-t-elle que le verrouillage d'un mutex verrouille une région de code jusqu'à ce que le mutex soit déverrouillé? Alors, comment les threads savent-ils que la région est verrouillée? [ MISE À JOUR: les threads savent que la région est verrouillée, par la séparation de la mémoire ]. Et un tel phénomène n'est-il pas censé s'appeler section critique? [ MISE À JOUR: les objets de section critique sont disponibles uniquement sous Windows, où les objets sont plus rapides que les mutex et ne sont visibles que par le thread qui les implémente. Sinon, la section critique fait simplement référence à la zone de code protégée par un mutex ]

En bref, pourriez-vous s'il vous plaît aider avec le programme d'exemple de mutex le plus simple possible et l' explication la plus simple possible sur la logique de son fonctionnement? Je suis sûr que cela aidera beaucoup d'autres débutants.

Nav
la source
2
Continuer à insister sur la nécessité d'un tutoriel simple (qu'il s'agisse de threads boost, tbb ou pthreads): Exemples de confusion: 1. stackoverflow.com/questions/3528877/... 2. stackoverflow.com/questions/2979525/… 3. stackoverflow.com/questions/2095977/to-mutex-or-not-to-mutex 4. stackoverflow.com/questions/3931026/... 5. stackoverflow.com/questions/1525189/...
Nav du
1
Je ne veux pas dire cela de manière offensive, mais ce que votre dernier commentaire me suggère, c'est que nous avons besoin de moins d'analogies et d'une meilleure explication technique du fonctionnement d'un mutex et des raisons pour lesquelles nous en avons besoin.
San Jacinto
@San: Pas de faute :) Mes commentaires visaient seulement à suggérer qu'un débutant pourrait obtenir l'explication la plus courte et la plus claire des mutex. De nombreuses analogies peuvent prêter à confusion pour le débutant, de sorte que différentes analogies doivent être conservées séparément. La raison pour laquelle j'ai posté les questions et les ans est que, en tant que débutant, j'ai trouvé difficile de lire de longues explications et des exemples de code. Je ne voudrais pas que quelqu'un d'autre subisse la douleur.
Nav
2
@Cory: Si cette réponse pouvait être améliorée, je serais heureux de prendre en compte vos suggestions. Je suis simplement heureux que beaucoup d'autres personnes l'aient trouvé utile. Si cela ne vous a pas aidé, il y a aussi des réponses d'autres personnes qui ont indiqué d'autres tutoriels sur les mutex. Pourquoi être si négatif?
Nav

Réponses:

278

Voici mon humble tentative d'expliquer le concept aux débutants du monde entier: (une version à code couleur sur mon blog également)

Beaucoup de gens courent vers une cabine téléphonique isolée (ils n'ont pas de téléphone portable) pour parler à leurs proches. La première personne à attraper la poignée de la porte de la cabine est celle qui est autorisée à utiliser le téléphone. Il doit continuer à s'accrocher à la poignée de la porte tant qu'il utilise le téléphone, sinon quelqu'un d'autre saisira la poignée, le jettera dehors et parlera à sa femme :) Il n'y a pas de système de file d'attente en tant que tel. Lorsque la personne a terminé son appel, sort de la cabine et quitte la poignée de la porte, la prochaine personne à saisir la poignée de la porte sera autorisée à utiliser le téléphone.

Un fil est: Chaque personne
Le mutex est: La poignée de la porte
La serrure est: La main de la personne
La ressource est: Le téléphone

Tout thread qui doit exécuter des lignes de code qui ne doivent pas être modifiées par d'autres threads en même temps (en utilisant le téléphone pour parler à sa femme), doit d'abord acquérir un verrou sur un mutex (serrant la poignée de la porte de la cabine. ). Ce n'est qu'alors qu'un thread pourra exécuter ces lignes de code (en passant l'appel téléphonique).

Une fois que le thread a exécuté ce code, il doit libérer le verrou sur le mutex afin qu'un autre thread puisse acquérir un verrou sur le mutex (d'autres personnes pouvant accéder à la cabine téléphonique).

[ Le concept d'avoir un mutex est un peu absurde quand on considère l'accès exclusif au monde réel, mais dans le monde de la programmation, je suppose qu'il n'y avait pas d'autre moyen de laisser les autres threads «voir» qu'un thread exécutait déjà certaines lignes de code. Il existe des concepts de mutex récursifs, etc., mais cet exemple était uniquement destiné à vous montrer le concept de base. J'espère que l'exemple vous donne une image claire du concept. ]

Avec le threading C ++ 11:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m;//you can use std::lock_guard if you want to be exception safe
int i = 0;

void makeACallFromPhoneBooth() 
{
    m.lock();//man gets a hold of the phone booth door and locks it. The other men wait outside
      //man happily talks to his wife from now....
      std::cout << i << " Hello Wife" << std::endl;
      i++;//no other thread can access variable i until m.unlock() is called
      //...until now, with no interruption from other men
    m.unlock();//man lets go of the door handle and unlocks the door
}

int main() 
{
    //This is the main crowd of people uninterested in making a phone call

    //man1 leaves the crowd to go to the phone booth
    std::thread man1(makeACallFromPhoneBooth);
    //Although man2 appears to start second, there's a good chance he might
    //reach the phone booth before man1
    std::thread man2(makeACallFromPhoneBooth);
    //And hey, man3 also joined the race to the booth
    std::thread man3(makeACallFromPhoneBooth);

    man1.join();//man1 finished his phone call and joins the crowd
    man2.join();//man2 finished his phone call and joins the crowd
    man3.join();//man3 finished his phone call and joins the crowd
    return 0;
}

Compilez et exécutez avec g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Au lieu d'utiliser explicitement locket unlock, vous pouvez utiliser des crochets comme indiqué ici , si vous utilisez un verrou à portée pour l'avantage qu'il offre . Les verrous à portée ont cependant une légère surcharge de performances.

Nav
la source
2
@San: Je vais être honnête; Oui, j'aime le fait que vous ayez fait de votre mieux pour expliquer les détails (avec flux) à un débutant complet. MAIS, (s'il vous plaît ne me comprenez pas mal), l'intention de cet article était de mettre le concept dans une courte explication (car les autres réponses pointaient vers de longs tutoriels). J'espère que cela ne vous dérangerait pas si je vous demande de copier l'intégralité de votre réponse et de la publier comme réponse distincte? Pour que je puisse revenir en arrière et modifier ma réponse pour indiquer votre réponse.
Nav
2
@Tom Dans ce cas, vous ne devriez pas accéder à ce mutex. Les opérations sur celui-ci devraient être encapsulées de sorte que tout ce qu'il garde soit protégé contre une telle folie. Si, lorsque vous utilisez l'API exposée de la bibliothèque, la bibliothèque est garantie pour les threads, vous pouvez inclure en toute sécurité un mutex distinct pour protéger vos propres éléments partagés. Sinon, vous ajoutez effectivement une nouvelle poignée de porte, comme vous l'avez suggéré.
San Jacinto
2
Pour étendre mon propos, ce que vous voudriez faire, c'est ajouter une autre pièce plus grande autour du stand. La chambre peut également contenir des toilettes et une douche. Disons qu'une seule personne est autorisée dans la salle à la fois. Vous devez concevoir la pièce de manière à ce que cette pièce ait une porte avec une poignée qui protège l'entrée de la pièce tout comme la cabine téléphonique. Alors maintenant, même si vous avez des mutex supplémentaires, vous pouvez réutiliser la cabine téléphonique dans n'importe quel projet. Une autre option serait d'exposer les mécanismes de verrouillage pour chaque appareil de la pièce et de gérer les serrures dans la classe de salle. Dans tous les cas, vous n'ajouteriez pas de nouveaux verrous au même objet.
San Jacinto
8
Votre exemple de thread C ++ 11 est incorrect . Il en va de même pour le TBB, l'indice est dans le nom de la serrure à portée .
Jonathan Wakely
3
Je connais bien les deux, @Jonathan. Vous semblez avoir manqué la phrase que j'ai écrite (could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer. Quant à l'utilisation du verrouillage de portée, c'est au développeur, en fonction du type d'application qu'il crée. Cette réponse visait à aborder la compréhension de base du concept de mutex et non à entrer dans toutes les complexités de celui-ci, donc vos commentaires et liens sont les bienvenus, mais un peu hors de la portée de ce tutoriel.
Nav
41

Bien qu'un mutex puisse être utilisé pour résoudre d'autres problèmes, la principale raison pour laquelle ils existent est de fournir une exclusion mutuelle et de résoudre ainsi ce que l'on appelle une condition de race. Lorsque deux (ou plus) threads ou processus tentent d'accéder à la même variable simultanément, nous avons un potentiel pour une condition de concurrence. Considérez le code suivant

//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
  i++;
}

Les éléments internes de cette fonction semblent si simples. Ce n'est qu'une seule déclaration. Cependant, un équivalent en langage pseudo-assembleur typique pourrait être:

load i from memory into a register
add 1 to i
store i back into memory

Étant donné que les instructions équivalentes en langage assembleur sont toutes nécessaires pour effectuer l'opération d'incrémentation sur i, nous disons que l'incrémentation de i est une opération non atmosphérique. Une opération atomique est une opération qui peut être effectuée sur le matériel avec une garantie de ne pas être interrompue une fois que l'exécution de l'instruction a commencé. L'incrémentation de i consiste en une chaîne de 3 instructions atomiques. Dans un système simultané où plusieurs threads appellent la fonction, des problèmes surviennent lorsqu'un thread lit ou écrit au mauvais moment. Imaginez que nous ayons deux threads exécutés simultanément et que l'un appelle la fonction immédiatement après l'autre. Disons aussi que nous avons initialisé i à 0. Supposons également que nous avons beaucoup de registres et que les deux threads utilisent des registres complètement différents, il n'y aura donc pas de collisions. Le moment réel de ces événements peut être:

thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1

Ce qui s'est passé, c'est que nous avons deux threads incrémentant i simultanément, notre fonction est appelée deux fois, mais le résultat est incompatible avec ce fait. Il semble que la fonction n'a été appelée qu'une seule fois. C'est parce que l'atomicité est "cassée" au niveau de la machine, ce qui signifie que les threads peuvent s'interrompre ou travailler ensemble aux mauvais moments.

Nous avons besoin d'un mécanisme pour résoudre ce problème. Nous devons imposer un ordre aux instructions ci-dessus. Un mécanisme courant consiste à bloquer tous les threads sauf un. Pthread mutex utilise ce mécanisme.

Tout thread qui doit exécuter des lignes de code qui peuvent modifier de manière non sécurisée les valeurs partagées par d'autres threads en même temps (en utilisant le téléphone pour parler à sa femme), doit d'abord être obligé d'acquérir un verrou sur un mutex. De cette façon, tout thread qui nécessite l'accès aux données partagées doit passer par le verrou mutex. Ce n'est qu'alors qu'un thread pourra exécuter le code. Cette section de code est appelée une section critique.

Une fois que le thread a exécuté la section critique, il doit libérer le verrou sur le mutex afin qu'un autre thread puisse acquérir un verrou sur le mutex.

Le concept d'avoir un mutex semble un peu étrange lorsque l'on considère les humains cherchant un accès exclusif à des objets réels et physiques, mais lors de la programmation, nous devons être intentionnels. Les fils et processus simultanés n'ont pas l'éducation sociale et culturelle que nous faisons, nous devons donc les forcer à partager les données correctement.

Alors techniquement, comment fonctionne un mutex? Ne souffre-t-il pas des mêmes conditions de course que nous avons évoquées plus tôt? Pthread_mutex_lock () n'est-il pas un peu plus complexe qu'un simple incrément d'une variable?

Techniquement parlant, nous avons besoin d'un support matériel pour nous aider. Les concepteurs de matériel nous donnent des instructions machine qui font plus d'une chose mais sont garanties atomiques. Un exemple classique d'une telle instruction est le test-and-set (TAS). Lorsque vous essayez d'acquérir un verrou sur une ressource, nous pouvons utiliser le TAS pour vérifier si une valeur en mémoire est 0. Si c'est le cas, ce serait notre signal que la ressource est en cours d'utilisation et que nous ne faisons rien (ou plus précisément , nous attendons par un mécanisme. Un mutex pthreads nous mettra dans une file d'attente spéciale dans le système d'exploitation et nous avertira lorsque la ressource sera disponible. Les systèmes plus stupides peuvent nous obliger à faire une boucle de rotation serrée, en testant la condition encore et encore) . Si la valeur en mémoire n'est pas 0, le TAS définit l'emplacement sur autre chose que 0 sans utiliser d'autres instructions. Il' C'est comme combiner deux instructions d'assemblage en 1 pour nous donner l'atomicité. Ainsi, le test et la modification de la valeur (si la modification est appropriée) ne peuvent pas être interrompus une fois qu'ils ont commencé. Nous pouvons construire des mutex en plus d'une telle instruction.

Remarque: certaines sections peuvent ressembler à une réponse précédente. J'ai accepté son invitation à éditer, il a préféré la manière originale, donc je garde ce que j'avais qui est imprégné d'un peu de son verbiage.

San Jacinto
la source
1
Merci beaucoup, San. J'ai lié à votre réponse :) En fait, j'avais l'intention que vous preniez ma réponse + votre réponse et que vous la publiiez comme une réponse séparée, pour garder le courant. Cela ne me dérange pas vraiment si vous réutilisez une partie de ma réponse. De toute façon, nous ne faisons pas cela pour nous-mêmes.
Nav
13

Le meilleur tutoriel sur les threads que je connaisse est ici:

https://computing.llnl.gov/tutorials/pthreads/

J'aime le fait qu'il soit écrit sur l'API, plutôt que sur une implémentation particulière, et cela donne de beaux exemples simples pour vous aider à comprendre la synchronisation.

R .. GitHub STOP AIDING ICE
la source
Je suis d'accord que c'est vraiment un bon tutoriel, mais c'est beaucoup d'informations sur une seule page et les programmes sont longs. La question que j'ai posée est la version mutex du discours "J'ai un rêve", où les débutants trouveraient un moyen simple d'en apprendre davantage sur les mutex et de comprendre le fonctionnement de la syntaxe non intuitive (c'est une explication qui manque dans tous les tutoriels) .
Nav le
7

Je suis tombé sur ce post récemment et je pense qu'il a besoin d'une solution mise à jour pour le mutex c ++ 11 de la bibliothèque standard (à savoir std :: mutex).

J'ai collé du code ci-dessous (mes premiers pas avec un mutex - j'ai appris la concurrence sur win32 avec HANDLE, SetEvent, WaitForMultipleObjects, etc.).

Puisque c'est ma première tentative avec std :: mutex et ses amis, j'aimerais voir les commentaires, suggestions et améliorations!

#include <condition_variable>
#include <mutex>
#include <algorithm>
#include <thread>
#include <queue>
#include <chrono>
#include <iostream>


int _tmain(int argc, _TCHAR* argv[])
{   
    // these vars are shared among the following threads
    std::queue<unsigned int>    nNumbers;

    std::mutex                  mtxQueue;
    std::condition_variable     cvQueue;
    bool                        m_bQueueLocked = false;

    std::mutex                  mtxQuit;
    std::condition_variable     cvQuit;
    bool                        m_bQuit = false;


    std::thread thrQuit(
        [&]()
        {
            using namespace std;            

            this_thread::sleep_for(chrono::seconds(5));

            // set event by setting the bool variable to true
            // then notifying via the condition variable
            m_bQuit = true;
            cvQuit.notify_all();
        }
    );


    std::thread thrProducer(
        [&]()
        {
            using namespace std;

            int nNum = 13;
            unique_lock<mutex> lock( mtxQuit );

            while ( ! m_bQuit )
            {
                while( cvQuit.wait_for( lock, chrono::milliseconds(75) ) == cv_status::timeout )
                {
                    nNum = nNum + 13 / 2;

                    unique_lock<mutex> qLock(mtxQueue);
                    cout << "Produced: " << nNum << "\n";
                    nNumbers.push( nNum );
                }
            }
        }   
    );

    std::thread thrConsumer(
        [&]()
        {
            using namespace std;
            unique_lock<mutex> lock(mtxQuit);

            while( cvQuit.wait_for(lock, chrono::milliseconds(150)) == cv_status::timeout )
            {
                unique_lock<mutex> qLock(mtxQueue);
                if( nNumbers.size() > 0 )
                {
                    cout << "Consumed: " << nNumbers.front() << "\n";
                    nNumbers.pop();
                }               
            }
        }
    );

    thrQuit.join();
    thrProducer.join();
    thrConsumer.join();

    return 0;
}
la nourriture pour poissons
la source
1
Super! Merci d'avoir posté. Bien que, comme je l'ai déjà mentionné, mon but était seulement d'expliquer simplement le concept de mutex. Tous les autres didacticiels ont rendu la tâche très difficile avec les concepts supplémentaires de consommateurs de producteurs et de variables de condition, etc., ce qui m'a rendu très difficile de comprendre ce qui se passait.
Nav du
4

La fonction pthread_mutex_lock()soit acquiert le mutex pour le thread appelant ou bloque le fil jusqu'à ce que le mutex peut être acquise. Les pthread_mutex_unlock()versions connexes du mutex.

Considérez le mutex comme une file d'attente; chaque thread qui tente d'acquérir le mutex sera placé à la fin de la file d'attente. Lorsqu'un thread libère le mutex, le thread suivant de la file d'attente se désactive et est maintenant en cours d'exécution.

Une section critique fait référence à une région de code où le non-déterminisme est possible. Cela est souvent dû au fait que plusieurs threads tentent d'accéder à une variable partagée. La section critique n'est pas sûre tant qu'une sorte de synchronisation n'est pas en place. Un verrou mutex est une forme de synchronisation.

chrisaycock
la source
1
Est-il garanti que la prochaine tentative de thread entrera exactement?
Arsen Mkrtchyan le
1
@Arsen Aucune garantie. C'est juste une analogie utile.
chrisaycock le
3

Vous êtes censé vérifier la variable mutex avant d'utiliser la zone protégée par le mutex. Ainsi, votre pthread_mutex_lock () pourrait (selon l'implémentation) attendre que mutex1 soit libéré ou renvoyer une valeur indiquant que le verrou ne pourrait pas être obtenu si quelqu'un d'autre l'a déjà verrouillé.

Mutex n'est en réalité qu'un sémaphore simplifié. Si vous lisez à leur sujet et que vous les comprenez, vous comprenez les mutex. Il y a plusieurs questions concernant les mutex et les sémaphores dans SO. Différence entre sémaphore binaire et mutex , quand devrions-nous utiliser mutex et quand devrions-nous utiliser sémaphore et ainsi de suite. L'exemple des toilettes dans le premier lien est à peu près aussi bon que l'on puisse penser. Tout ce que le code fait est de vérifier si la clé est disponible et si c'est le cas, de la réserver. Notez que vous ne réservez pas vraiment les toilettes elles-mêmes, mais la clé.

Makis
la source
1
pthread_mutex_lockne peut pas revenir si quelqu'un d'autre tient la serrure. Cela bloque dans ce cas et c'est tout le problème. pthread_mutex_trylockest la fonction qui reviendra si le verrou est maintenu.
R .. GitHub STOP HELPING ICE
1
Ouais, je n'avais pas réalisé au début de quelle mise en œuvre il s'agissait.
Makis le
3

Pour ceux qui recherchent l'exemple du mutex shortex:

#include <mutex>

int main() {
    std::mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();
}
Néant
la source