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

Я новичок в многопоточности и пытаюсь понять, как работают мьютексы. Было много Googling и Я нашел достойный учебник, но он все же оставил некоторые сомнения в том, как он работает, потому что я создал свою собственную программу, в которой блокировка не работает.

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

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

Ответ 1

Разрешения на объяснение и код наиболее приветствуются (чтобы сделать его более четким/короче/проще/правильнее).
Вот моя скромная попытка объяснить концепцию новичков по всему миру: (a цветная версия в моем блоге тоже)

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

A thread: каждый человек мьютекс: Дверная ручка
блокировка: рука человека
ресурс: Телефон

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

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

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

С потоком С++ 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 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

С 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 устарел. Замена показана здесь.
Кроме того, вместо явного использования lock и unlock вы можете использовать скобки как показано здесь, если вы используете блокировку с привязкой за то преимущество, которое он предоставляет.

Ответ 2

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

//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

Поскольку для выполнения операции инкремента в я требуются эквивалентные инструкции на языке ассемблера, мы говорим, что приращение я является неамонической операцией. Атомная операция - это операция, которая может быть завершена на аппаратном обеспечении с гарантией того, что она не будет прервана после начала выполнения инструкции. Инкремент я состоит из цепочки из трех атомных инструкций. В параллельной системе, где несколько потоков вызывают функцию, возникают проблемы, когда поток читает или записывает в неподходящее время. Представьте, что мы имеем два потока, работающих одновременно, и один вызывает функцию сразу после другого. Позвольте также сказать, что мы инициализировали я для 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 поместит нас в специальную очередь в операционной системе и уведомит нас, когда ресурс станет доступным. Системы Dumber могут потребовать от нас выполнения жесткой петли спина, проверяя состояние снова и снова), Если значение в памяти не равно 0, TAS устанавливает местоположение в нечто, отличное от 0, без использования каких-либо других инструкций. Это похоже на объединение двух команд сборки в 1, чтобы дать нам атомарность. Таким образом, тестирование и изменение значения (если изменение соответствует) не может быть прервано после его начала. Мы можем построить мьютексы поверх этой инструкции.

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

Ответ 3

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

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

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

Ответ 4

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

Я вставил код ниже (мои первые шаги с мьютексом - я узнал concurrency на 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;
}

Ответ 5

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

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

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

Ответ 6

Вы должны проверить переменную mutex перед использованием области, защищенной мьютексом. Таким образом, ваш pthread_mutex_lock() может (в зависимости от реализации) ждать, пока mutex1 не будет выпущен, или не вернет значение, указывающее, что блокировка не может быть получена, если кто-то еще ее заблокировал.

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