Mutex пример / учебник? - PullRequest
       34

Mutex пример / учебник?

156 голосов
/ 14 февраля 2011

Я новичок в многопоточности и пытался понять, как работают мьютексы.Я много гуглил и Я нашел приличное учебное пособие , но оно все еще оставляло некоторые сомнения в том, как оно работает, потому что я создал свою собственную программу, в которой блокировка не работала.неинтуитивный синтаксис мьютекса - pthread_mutex_lock( &mutex1 );, где похоже, что мьютекс заблокирован, когда я действительно хочу заблокировать какую-то другую переменную.Означает ли этот синтаксис, что блокировка мьютекса блокирует область кода, пока мьютекс не будет разблокирован?Тогда откуда потоки узнают, что регион заблокирован?[ ОБНОВЛЕНИЕ: потоки знают, что регион заблокирован, Ограждение памяти ].И разве такое явление не должно называться критическим разделом?[ ОБНОВЛЕНИЕ: объекты критических разделов доступны только в Windows, где объекты быстрее мьютексов и видны только потоку, который их реализует.В противном случае критический раздел относится только к области кода, защищенной мьютексом ]

Короче, не могли бы вы помочь с самой простой из возможных мьютексов пример программы и самой простой из возможных объяснение о логике того, как это работает?Я уверен, что это поможет множество других новичков.

Ответы [ 8 ]

243 голосов
/ 01 марта 2011

Вот моя скромная попытка объяснить концепцию новичкам по всему миру: ( версия с цветовой кодировкой в моем блоге тоже)

Многие люди бегут к одинокой телефонной будке (без мобильных телефонов), чтобы поговорить со своими близкими. Первый, кто поймает дверную ручку будки, это тот, кому разрешено пользоваться телефоном. Он должен держаться за ручку двери, пока он пользуется телефоном, в противном случае кто-то другой схватится за ручку, выбросит его и поговорит с женой :) Система очередей как таковая отсутствует. Когда человек заканчивает разговор, выходит из кабины и покидает дверную ручку, следующий человек, который схватит дверную ручку, получит право пользоваться телефоном.

A нить это: Каждый человек
mutex - это: Дверная ручка
замок - это: рука человека
Ресурс : Телефон

Любой поток, который должен выполнить некоторые строки кода, которые не должны изменяться другими потоками одновременно (используя телефон для разговора с женой), должен сначала получить блокировку мьютекса (сжимая дверную ручку стенда). Только тогда поток сможет запустить эти строки кода (сделать телефонный звонок).

Как только поток выполнил этот код, он должен снять блокировку с мьютекса, чтобы другой поток мог получить блокировку с мьютексом (другие люди смогут получить доступ к телефонной будке).

[ Концепция наличия мьютекса несколько абсурдна, если рассматривать реальный эксклюзивный доступ, но в мире программирования, я думаю, не было другого способа позволить другим потокам «увидеть», что поток уже был выполнение некоторых строк кода. Существуют концепции рекурсивных мьютексов и т. Д., Но этот пример предназначен только для того, чтобы показать вам базовую концепцию. Надеюсь, что пример дает вам четкое представление о концепции. ]

С потоками 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;
}

Скомпилируйте и запустите, используя g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Вместо явного использования lock и unlock вы можете использовать скобки , как показано здесь , если вы используете блокировку с ограничением для преимущества, которое оно обеспечивает . Однако замки с ограниченным пространством имеют незначительную производительность.

С TBB: Вам понадобится TBB , чтобы запустить приведенную ниже программу, но цель публикации кода TBB состоит в том, что вы понимаете последовательность блокировок и разблокировок, просто взглянув на простой код (возможно, показанная блокировка с используя приобретение и выпуск - , что также является исключительной безопасностью -, но это более понятно).

#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();
}

Обратите внимание, что tbb_thread.h устарело. Замена показана здесь .

35 голосов
/ 01 марта 2011

Хотя мьютекс может использоваться для решения других проблем, основная причина, по которой они существуют, заключается в том, чтобы обеспечить взаимное исключение и тем самым решить так называемое состояние гонки. Когда два (или более) потока или процесса пытаются получить доступ к одной и той же переменной одновременно, у нас есть потенциал для состояния гонки. Рассмотрим следующий код

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

Внутренности этой функции выглядят так просто. Это только одно утверждение. Однако типичный эквивалент псевдо-ассемблера может быть следующим:

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

Поскольку все эквивалентные инструкции на языке ассемблера требуются для выполнения операции увеличения на i, мы говорим, что увеличение i является неатмосовой операцией. Элементарная операция - это операция, которая может быть выполнена на оборудовании с гарантией того, что она не будет прервана после начала выполнения инструкции. Инкремент i состоит из цепочки из 3 атомарных инструкций. В параллельной системе, где несколько потоков вызывают функцию, проблемы возникают, когда поток читает или пишет в неправильное время. Представьте, что у нас одновременно работают два потока, и один вызывает функцию сразу после другого. Предположим также, что мы инициализировали значение 0. Также предположим, что у нас много регистров и что два потока используют совершенно разные регистры, поэтому коллизий не будет. Фактическое время этих событий может быть:

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

То, что произошло, это то, что у нас есть два потока, которые увеличиваются одновременно, наша функция вызывается дважды, но результат не согласуется с этим фактом. Похоже, функция была вызвана только один раз. Это связано с тем, что атомарность «нарушена» на уровне машины, что означает, что потоки могут прерывать друг друга или работать вместе в неподходящее время.

Нам нужен механизм, чтобы решить это. Нам нужно навести порядок в инструкциях выше. Одним из распространенных механизмов является блокирование всех потоков, кроме одного. Pthread мьютекс использует этот механизм.

Любой поток, который должен выполнить некоторые строки кода, которые могут небезопасно изменять совместно используемые значения другими потоками одновременно (используя телефон, чтобы поговорить с его женой), должен сначала получить блокировку на мьютекс. Таким образом, любой поток, которому требуется доступ к общим данным, должен пройти через блокировку мьютекса. Только тогда поток сможет выполнить код. Этот раздел кода называется критическим разделом.

Как только поток выполнил критическую секцию, он должен снять блокировку с мьютекса, чтобы другой поток мог получить блокировку с мьютексом.

Концепция наличия мьютекса кажется несколько странной, если учесть, что люди ищут эксклюзивный доступ к реальным, физическим объектам, но при программировании мы должны быть намеренными. Параллельные потоки и процессы не имеют социального и культурного воспитания, которое мы делаем, поэтому мы должны заставить их обмениваться данными.

Итак, технически говоря, как работает мьютекс? Разве он не страдает от тех же рас, что мы упоминали ранее? Разве pthread_mutex_lock () не немного сложнее простого приращения переменной?

Технически говоря, нам нужна некоторая аппаратная поддержка, чтобы помочь нам. Разработчики аппаратного обеспечения дают нам машинные инструкции, которые делают больше чем одно, но гарантируют, что они будут атомарными. Классическим примером такой инструкции является тест-и-набор (TAS). При попытке получить блокировку ресурса мы могли бы использовать TAS, чтобы проверить, равно ли значение в памяти 0. Если это так, это будет нашим сигналом, что ресурс используется, и мы ничего не делаем (или, точнее, , мы ждем по какому-то механизму. Мьютекс pthreads поместит нас в специальную очередь в операционной системе и уведомит нас, когда ресурс станет доступным. Более тупые системы могут потребовать, чтобы мы выполняли жесткий цикл вращения, проверяя условие снова и снова) , Если значение в памяти не равно 0, TAS устанавливает местоположение в значение, отличное от 0, без использования каких-либо других инструкций. Это как объединение двух инструкций по сборке в 1, чтобы придать нам атомарность. Таким образом, тестирование и изменение значения (если изменение уместно) не может быть прервано после его начала. Мы можем создать мьютексы поверх такой инструкции.

Примечание: некоторые разделы могут выглядеть аналогично предыдущему ответу. Я принял его приглашение отредактировать, он предпочел оригинальную версию, так что я сохраняю то, что у меня было, и немного словоблудия.

11 голосов
/ 14 февраля 2011

Лучшее учебное пособие по темам, которое я знаю, находится здесь:

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

Мне нравится, что он написан об API, а не о конкретной реализации, и он дает несколько простых простых примеров, которые помогут вам понять синхронизацию.

7 голосов
/ 13 ноября 2012

Я недавно наткнулся на этот пост и думаю, что ему нужно обновленное решение для мьютекса c ++ 11 стандартной библиотеки (а именно std :: mutex).

Я вставил код ниже (мои первые шаги с мьютексом - я изучил параллелизм на win32 с HANDLE, SetEvent, WaitForMultipleObjects и т. Д.).

Так как это моя первая попытка с std :: mutex и друзьями, я хотел бы видеть комментарии, предложения и улучшения!

#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;
}
4 голосов
/ 14 февраля 2011

Функция pthread_mutex_lock() либо получает мьютекс для вызывающего потока, либо блокирует поток до тех пор, пока мьютекс не будет получен.Связанный pthread_mutex_unlock() освобождает мьютекс.

Думайте о мьютексе как о очереди;каждый поток, который пытается получить мьютекс, будет помещен в конец очереди.Когда поток освобождает мьютекс, следующий поток в очереди отключается и теперь работает.

A критическая секция относится к области кода, где возможен недетерминизмЧасто это происходит потому, что несколько потоков пытаются получить доступ к общей переменной.Критическая секция небезопасна, пока не установлена ​​какая-то синхронизация.Блокировка мьютекса - это одна из форм синхронизации.

3 голосов
/ 14 февраля 2011

Вы должны проверить переменную мьютекса перед использованием области, защищенной мьютексом.Таким образом, ваш pthread_mutex_lock () может (в зависимости от реализации) дождаться освобождения mutex1 или вернуть значение, указывающее, что блокировка не может быть получена, если кто-то другой уже заблокировал ее.Если вы читаете о них и понимаете их, вы понимаете мьютексы.Есть несколько вопросов, касающихся мьютексов и семафоров в SO. Разница между двоичным семафором и мьютексом , Когда мы должны использовать мьютекс и когда мы должны использовать семафор и так далее.Пример туалета в первой ссылке примерно такой же хороший пример, как можно себе представить.Все, что нужно сделать, это проверить, доступен ли ключ, и если он есть, зарезервировать его.Обратите внимание, что вы действительно не забронируете сам туалет, а ключ.

2 голосов
/ 03 августа 2015

ПРИМЕР СЕМЕРА ::

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

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

Ссылка: http://pages.cs.wisc.edu/~remzi/Classes/537/Fall2008/Notes/threads-semaphores.txt

0 голосов
/ 24 июля 2018

Для тех, кто ищет пример мьютекса с шортексом:

#include <mutex>
using namespace std;

int main() {
    mutex m;

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

    return 0;
}
...