Mutex exemple / tutoriel?

Je suis nouveau dans le multithreading et essayais de comprendre comment les mutex fonctionnent. J’ai fait beaucoup de recherches sur Google et j’ai trouvé un tutoriel décent , mais cela laissait encore des doutes quant à son fonctionnement car j’ai créé mon propre programme dans lequel le locking ne fonctionnait pas.

Une syntaxe absolument non intuitive du mutex est pthread_mutex_lock( &mutex1 ); , où il semble que le mutex soit verrouillé, lorsque ce que je veux vraiment verrouiller est une autre variable. Cette syntaxe signifie-t-elle que le locking 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, par un système de clôture de mémoire ]. Et un tel phénomène n’est-il pas censé s’appeler section critique? [ UPDATE: les objects de section critiques sont disponibles uniquement sous Windows, où les objects 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 nous 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.

Voici mon humble tentative d’expliquer le concept aux débutants du monde entier: (une version avec code couleur sur mon blog aussi)

Beaucoup de gens courent à une seule cabine téléphonique (pas de téléphone portable) pour parler à leurs proches. La première personne à attraper la poignée de la cabine est celle qui est autorisée à utiliser le téléphone. Il doit continuer à tenir la poignée de la porte aussi longtemps qu’il utilise le téléphone, sinon quelqu’un d’autre va attraper la poignée, le jeter dehors et parler à sa femme 🙂 Il n’y a pas de système de queue en tant que tel. Lorsque la personne termine 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 porte
La serrure est la main de la personne
La ressource est: le téléphone

Tout thread devant exécuter des lignes de code qui ne doivent pas être modifiées par d’autres threads en même temps (utiliser le téléphone pour parler à sa femme) doit d’abord acquérir un verrou sur un mutex (en tenant la poignée de la cabine) ). Alors seulement, un thread pourra exécuter ces lignes de code (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 (les 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’access 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 voir les threads exécuter des lignes de code. Il y a des concepts de mutexes 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  #include  #include  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; } 

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

Au lieu d'utiliser explicitement le lock et le unlock , vous pouvez utiliser des crochets comme indiqué ici , si vous utilisez un verrou de scope pour l'avantage qu'il offre . Les verrous de scope ont cependant une légère surcharge de performance.

Avec TBB: Vous aurez besoin de TBB pour exécuter le programme ci-dessous, mais le but de poster du code TBB est que vous compreniez la séquence de locking et de délocking simplement en regardant le code simple. release - qui est aussi sûre des exceptions -, mais c'est plus clair).

 #include  #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 "< 

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

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

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

Les composants internes de cette fonction sont si simples. C’est seulement une déclaration. Cependant, un équivalent de langage de pseudo-assemblage typique pourrait être:

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

Comme les instructions équivalentes en langage assembleur sont toutes requirejses 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. Imaginez que nous ayons deux threads en cours d’exécution simultanée et que l’un appelle la fonction immédiatement après l’autre. Disons aussi que je ai initialisé à 0. Supposons également que nous avons beaucoup de registres et que les deux threads utilisent des registres complètement différents, donc il n’y aura pas de collisions. Le calendrier 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é 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 au mauvais moment.

Nous avons besoin d’un mécanisme pour résoudre ce problème. Nous devons imposer des commandes 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 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 verrouillé sur un mutex. De cette manière, tout thread nécessitant un access 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 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 qui cherchent un access exclusif à des objects physiques réels, mais lors de la programmation, nous devons être intentionnels. Les discussions et les processus concomitants ne bénéficient pas de l’éducation sociale et culturelle que nous faisons, nous devons donc les forcer à partager des données avec brio.

Donc, techniquement parlant, 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 parlant, nous avons besoin d’un support matériel pour nous aider. Les concepteurs de matériel nous donnent des instructions sur les machines qui font plus d’une chose mais qui sont garanties atomiques. Un exemple classique d’une telle instruction est le test-and-set (TAS). En essayant d’acquérir un verrou sur une ressource, nous pouvons utiliser le TAS pour vérifier si une valeur en mémoire est égale à 0. Si tel est le cas, cela signifierait 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 placera dans une queue spéciale dans le système d’exploitation et nous avisera de la disponibilité de la ressource. . 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 1 pour nous donner une atomicité. Ainsi, tester et modifier la valeur (si elle est appropriée) ne peut pas être interrompue une fois qu’elle a commencé. Nous pouvons construire des mutex en plus d’une telle instruction.

Remarque: certaines sections peuvent sembler similaires à une réponse antérieure. J’ai accepté son invitation à éditer, il a préféré la manière originale, donc je garde ce que j’ai eu qui est infusé avec un peu de son verbiage.

Le meilleur tutoriel de threads que je connaisse est ici:

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

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

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 mes amis, j’aimerais voir des commentaires, des suggestions et des améliorations!

 #include  #include  #include  #include  #include  #include  #include  int _tmain(int argc, _TCHAR* argv[]) { // these vars are shared among the following threads std::queue 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 lock( mtxQuit ); while ( ! m_bQuit ) { while( cvQuit.wait_for( lock, chrono::milliseconds(75) ) == cv_status::timeout ) { nNum = nNum + 13 / 2; unique_lock qLock(mtxQueue); cout << "Produced: " << nNum << "\n"; nNumbers.push( nNum ); } } } ); std::thread thrConsumer( [&]() { using namespace std; unique_lock lock(mtxQuit); while( cvQuit.wait_for(lock, chrono::milliseconds(150)) == cv_status::timeout ) { unique_lock qLock(mtxQueue); if( nNumbers.size() > 0 ) { cout << "Consumed: " << nNumbers.front() << "\n"; nNumbers.pop(); } } } ); thrQuit.join(); thrProducer.join(); thrConsumer.join(); return 0; } 

La fonction pthread_mutex_lock() acquiert le mutex pour le thread appelant ou le bloque jusqu’à ce que le mutex puisse être acquis. Le pthread_mutex_unlock() associé pthread_mutex_unlock() libère le mutex.

Pensez au mutex comme une queue; chaque thread qui tente d’acquérir le mutex sera placé à la fin de la queue. Lorsqu’un thread libère le mutex, le prochain thread de la queue se désactive et est 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 jusqu’à ce qu’une sorte de synchronisation soit en place. Un verrou mutex est une forme de synchronisation.

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 n’a pas pu être obtenu si quelqu’un l’a déjà verrouillé.

Mutex est vraiment un sémaphore simplifié. Si vous lisez à leur sujet et les comprenez, vous comprenez les mutex. Il existe plusieurs questions concernant les mutex et les sémaphores dans SO. Différence entre le sémaphore binary et le mutex , Quand devrions-nous utiliser mutex et quand devrions-nous utiliser le sémaphore , etc. L’exemple de canvastte dans le premier lien est un exemple à peu près aussi bon qu’on puisse le penser. Tout le code fait pour vérifier si la clé est disponible et si c’est le cas, le réserve. Notez que vous ne réservez pas vraiment la canvastte elle-même, mais la clé.

SEMAPHORE EXEMPLE ::

 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

Pour ceux qui recherchent l’exemple de shortex mutex:

 #include  using namespace std; int main() { mutex m; m.lock(); // do thread-safe stuff m.unlock(); return 0; }