web-dev-qa-db-fra.com

Mutex exemple/tutoriel?

Je suis novice en multithreading et j'essayais de comprendre le fonctionnement des mutex. J'ai fait beaucoup de recherches sur Google et j'ai trouvé un tutoriel décent , mais cela laisse quelques 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 bloqué, alors que 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? [UPDATE: les threads savent que la région est verrouillée, parMemory Fencing ]. Et un tel phénomène n’est-il pas supposé être appelé section critique? [UPDATE: les objets de section critiques sont disponibles uniquement sous Windows, où ils sont plus rapides que les mutex et ne sont visibles que par le thread qui les implémente. Sinon, la section critique fait uniquement 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 plus simple possible mutex programme exemple et le plus simple possible explication sur la logique de son fonctionnement? Je suis sûr que cela aidera beaucoup des autres débutants.

151
Nav

Voici mon humble tentative d'explication du concept aux débutants du monde entier: (une version codée en couleur sur mon blog aussi))

Beaucoup de gens se rendent à une cabine téléphonique isolée (sans 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 rester accroché à la poignée de la porte tant qu'il utilise le téléphone, sinon quelqu'un d'autre l'attrapera, le jettera à l'extérieur 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 à qui elle pourra mettre la main sur la poignée de la porte sera autorisée à utiliser le téléphone. 

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

Tout thread qui doit exécuter certaines lignes de code et qui ne devrait pas être modifié en même temps par d'autres threads (en utilisant le téléphone pour parler à sa femme) doit d'abord acquérir un verrou sur un mutex (serrer la poignée de la porte de la cabine ). Alors seulement, un thread pourra exécuter ces lignes de code (passer un appel téléphonique). 

Une fois que le thread a exécuté ce code, il devrait 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 lorsqu'on envisage un accès exclusif dans le 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 Il existe des concepts de mutexes récursifs, etc., mais cet exemple était uniquement destiné à vous montrer le concept de base. J'espère que cet exemple vous en donne une image claire.

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;
}

Compiler et exécuter en utilisant g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Au lieu d'utiliser explicitement lock et unlock, vous pouvez utiliser des crochets comme indiqué ici , si vous utilisez un verrou limité pour l'avantage qu'il procure . Les verrous Scoped ont toutefois une légère surcharge de performances. 

Avec TBB: Vous aurez besoin de TBB pour exécuter le programme ci-dessous, mais le but de publier du code TBB est que vous compreniez la séquence de verrouillage et de déverrouillage en regardant simplement code (aurait pu montrer le verrouillage ciblé en n'utilisant pas les fonctions d'acquisition et de libération - qui est également sans exception -, mais cela est plus clair).

#include <iostream>
#include "/tbb/mutex.h"
#include "/tbb/tbb_thread.h"
using namespace tbb;

typedef mutex myMutex;
static myMutex sm;
int i = 0;

void someFunction() 
{ 
      //Note: Since a scoped lock is used below, you should know that you 
      //can specify a scope for the mutex using curly brackets, instead of 
      //using lock.acquire() and lock.release(). The lock will automatically 
      //get released when program control goes beyond the scope.
      myMutex::scoped_lock lock;//create a lock
      lock.acquire(sm);//Method acquire waits until it can acquire a lock on the mutex
         //***only one thread can access the lines from here...***
         ++i;//incrementing i is safe (only one thread can execute the code in this scope) because the mutex locked above protects all lines of code until the lock release.
         sleep(1);//simply creating a delay to show that no other thread can increment i until release() is executed
         std::cout<<"In someFunction "<<i<<"\n";
         //***...to here***
      lock.release();//releases the lock (duh!)      
}

int main()
{
   tbb_thread my_thread1(someFunction);//create a thread which executes 'someFunction'
   tbb_thread my_thread2(someFunction);
   tbb_thread my_thread3(someFunction);

   my_thread1.join();//This command causes the main thread (which is the 'calling-thread' in this case) to wait until thread1 completes its task.
   my_thread2.join();
   my_thread3.join();
}

Notez que tbb_thread.h est obsolète. Le remplacement est montré ici

233
Nav

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

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

L'intérieur de cette fonction a l'air si simple. Ce n'est qu'une déclaration. Cependant, un pseudo-assembleur typique peut être:

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

Les instructions équivalentes en langage assembleur étant toutes nécessaires pour effectuer l'opération d'incrémentation sur i, nous disons que l'incrémentation 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 l'exécution de l'instruction commencée. L'incrémentation i consiste en une chaîne de 3 instructions atomiques. Dans un système concurrent où plusieurs threads appellent la fonction, des problèmes surviennent lorsqu'un thread lit ou écrit au mauvais moment. Imaginons que deux threads s'exécutent simultanément et que l'un appelle la fonction immédiatement après l'autre. Disons également que nous avons initialisé à 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 est arrivé, c’est que nous avons deux threads incrémentant i simultanément, notre fonction est appelée deux fois, mais le résultat est incohérent avec ce fait. Il semble que la fonction n'ait été appelée qu'une fois. En effet, l'atomicité est "interrompue" au niveau de la machine, ce qui signifie que les threads peuvent s'interrompre les uns les autres ou fonctionner ensemble au mauvais moment.

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 commun consiste à bloquer tous les threads sauf un. Le mutex Pthread utilise ce mécanisme.

Tout thread qui doit exécuter des lignes de code susceptibles de modifier de manière non sécurisée des valeurs partagées par d'autres threads en même temps (en utilisant le téléphone pour parler à sa femme) doit d'abord se faire acquérir un verrou sur un mutex. De cette manière, tout thread nécessitant un accès aux données partagées doit passer par le verrou mutex. Alors seulement, un thread pourra exécuter le code. Cette section de code s'appelle une section critique.

Une fois que le thread a exécuté la section critique, il devrait 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 lorsqu'on considère des humains cherchant un accès exclusif à des objets réels et physiques, mais lors de la programmation, nous devons être intentionnels. Les processus et threads concurrents n'ont pas l'éducation sociale et culturelle que nous avons, nous devons donc les obliger à partager les données correctement.

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

Techniquement, nous avons besoin d’un support matériel pour nous aider. Les concepteurs de matériel informatique nous donnent des instructions machine qui font plus d’une chose mais qui sont garanties d’être atomiques. Un exemple classique d'une telle instruction est le test-and-set (TAS). Lorsque vous tentez d’obtenir un verrou sur une ressource, vous pouvez utiliser le système TAS pour vérifier si une valeur en mémoire est 0. Si tel est le cas, cela indiquerait que la ressource est utilisée et que nous ne faisons rien (ou plus précisément , nous attendons par un mécanisme quelconque. Un mutex pthreads nous place dans une file d’attente spéciale du système d’exploitation et nous avertit lorsque la ressource devient disponible. . 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. C'est comme combiner deux instructions d'assemblage en une 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 que celle-ci a commencé. Nous pouvons construire des mutex au-dessus d'une telle instruction.

Remarque: certaines sections peuvent sembler similaires à une réponse précédente. J'ai accepté son invitation au montage, il a préféré la manière originale, alors je garde ce que j'avais, infusé d'un peu de son verbiage.

34
San Jacinto

Le meilleur tutoriel 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 donne quelques exemples simples pour vous aider à comprendre la synchronisation.

11
R..

Je suis tombé sur ce message 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.).

Comme il s’agit de ma première tentative avec std :: mutex et ses amis, j'aimerais connaître les commentaires, les suggestions et les 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;
}
7
fishfood

La fonction pthread_mutex_lock() soit acquiert le mutex pour le thread appelant, soit le bloque jusqu'à ce que le mutex puisse être acquis. La pthread_mutex_unlock() associée libère le mutex.

Pensez au 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 prochain thread de la file d'attente est désactivé et s'exécute maintenant.

Une section 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 jusqu’à ce qu’une sorte de synchronisation soit en place. Un verrou mutex est une forme de synchronisation.

4
chrisaycock

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

Mutex est vraiment juste 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 le sémaphore binaire et le mutex , Quand devrions-nous utiliser un mutex et quand devrions-nous utiliser un sémaphore et ainsi de suite. L’exemple des toilettes du premier lien est un exemple aussi valable que celui auquel on peut penser. Tout le code fait est de vérifier si la clé est disponible et si c'est le cas, la réserve. Notez que vous ne réservez pas vraiment les toilettes elles-mêmes, mais la clé.

3
Makis

EXEMPLE DE SEMAPHORE ::

sem_t m;
sem_init(&m, 0, 0); // initialize semaphore to 0

sem_wait(&m);
// critical section here
sem_post(&m);

Référence: http://pages.cs.wisc.edu/~remzi/Classes/537/Fall2008/Notes/threads-semaphores.txt

2
parasrish

Pour ceux qui recherchent l'exemple du mutex shortex:

#include <mutex>
using namespace std;

int main() {
    mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();

    return 0;
}
0
nathangeorge1