Приклад / підручник Mutex? [зачинено]


176

Я новачок у багатопотоковому читанні і намагався зрозуміти, як працюють мутекси. Зробив багато в Google, але все ще залишив сумніви в тому, як це працює, тому що я створив власну програму, в якій блокування не працювало.

Один абсолютно неінтуїтивний синтаксис мютексу - це те pthread_mutex_lock( &mutex1 );, де схоже, що мютекс заблокований, коли те, що я дійсно хочу заблокувати, - це якась інша змінна. Чи означає цей синтаксис, що блокування mutex блокує область коду, поки не відкривається mutex? Тоді як нитки знають, що область заблокована? [ ОНОВЛЕННЯ: Нитки знають, що область заблокована за допомогою фехтування пам'яті ]. І чи не таке явище слід назвати критичним розділом? [ ОНОВЛЕННЯ: Об'єкти критичних розділів доступні лише в Windows, де об'єкти швидші, ніж мутекси, і видимі лише для потоку, який його реалізує. В іншому випадку критичний розділ якраз посилається на область коду, захищену mutex ]

Якщо коротко, чи можете ви допомогти з найпростішою можливою програмою mutex та найпростішим поясненням логіки того, як вона працює? Я впевнений, що це допоможе безлічі інших новачків.


2
Продовжуючи наголошувати на необхідності простого підручника (будь то збільшити теми, tbb або pthreads): Приклади плутанини: 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

1
Я не маю на увазі це образливо, але те, що ваш останній коментар пропонує мені, - це те, що нам потрібно менше аналогій і кращого технічного пояснення того, як працює мютекс і для чого вони потрібні.
San Jacinto

@San: Без правопорушень :) Мої коментарі мали на меті лише підказати, що новачок може отримати найкоротше, чітке пояснення мютексів. Багато аналогій можуть стати заплутаними для новачків, тому різні аналогії слід зберігати окремо. Вся причина, коли я публікую запитання і відповіді, полягає в тому, що, як новачок, я відчув біль читати довгі пояснення та зразки коду. Я не хотів би, щоб хтось інший переживав біль.
Nav

2
@Cory: Якщо цю відповідь можна покращити, я з радістю прийму ваші пропозиції. Я просто радий, що багато інших людей вважають це корисним. Якщо це вам не допомогло, є відповіді і від інших людей, які вказали на інші підручники з мютексу. Чому бути таким негативним?
Nav

Відповіді:


278

Ось моя скромна спроба пояснити концепцію новачкам у всьому світі: ( кольорова версія в моєму блозі)

Багато людей біжать до самотньої телефонної будки (у них немає мобільних телефонів), щоб поговорити зі своїми близькими. Першою людиною, яка спіймала ручку дверей кабінки, є той, кому дозволено користуватися телефоном. Йому слід триматись за ручку дверей, поки він користується телефоном, інакше хтось інший схопить за ручку, викине його та поспілкується зі своєю дружиною :) Системи черг як такої немає. Коли людина закінчить свій дзвінок, виходить із кабінки та відходить від дверної ручки, наступній людині, яка влаштується за ручку дверей, буде дозволено користуватися телефоном.

Нитка є: Кожна людина м'ютекс є наступним : дверна ручка замок є: рука людини ресурс є: Телефон


Будь-яка нитка, яка повинна виконувати деякі рядки коду, які не повинні одночасно змінюватись іншими потоками (користуючись телефоном для розмови з дружиною), спочатку повинна придбати замок на мютекс (стискаючи ручку дверей кабінки ). Лише тоді потік зможе запустити ці рядки коду (здійснювати телефонний дзвінок).

Після того, як потік виконав цей код, він повинен звільнити замок на mutex, щоб інший потік міг придбати замок на 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, ви можете використовувати дужки, як показано тут , якщо ви використовуєте обмежений діапазон для переваги, яку він надає . Замкові замки мають невеликі експлуатаційні витрати, проте.


2
@San: Я буду чесною; Так, мені подобається те, що ви постаралися пояснити деталі (з потоком) для повного новачка. Але, (будь ласка, не розумійте мене), намір цієї посади полягав у тому, щоб викласти концепцію в короткому поясненні (адже інші відповіді вказували на довгі уроки). Сподіваюся, ви не заперечуєте, якщо я попрошу скопіювати всю свою відповідь і опублікувати її як окрему відповідь? Щоб я міг відкатати і відредагувати свою відповідь, щоб вказати на вашу відповідь.
Nav

2
@Tom У такому випадку ви не повинні мати доступ до цього файлу. Операції на ньому повинні бути капсульовані таким чином, щоб все, що воно охороняло, було захищене від такої надмоги. Якщо ви використовуєте відкритий API бібліотеки, бібліотека гарантовано захищена від потоків, тоді ви можете безпечно включити різний файл mutex для захисту власних спільних елементів. В іншому випадку ви дійсно додаєте нову ручку дверей, як ви запропонували.
San Jacinto

2
Щоб продовжити свою думку, те, що ви хочете зробити, - це додати ще одну, більшу кімнату навколо кабінки. У номері також може бути туалет і душ. Скажімо, що в кімнату допускається відразу 1 людина. Ви повинні спроектувати кімнату так, щоб у цій кімнаті були двері з ручкою, яка захищає вхід до приміщення, як і телефонна будка. Тож тепер, навіть якщо у вас є додаткові мутекси, ви можете повторно використовувати телефонну кабінку в будь-якому проекті. Іншим варіантом було б розкрити механізми блокування кожного пристрою в кімнаті та керувати замками в кімнатному класі. Так чи інакше, ви не додавали б нові блокування до одного об’єкта.
San Jacinto

8
Ваш приклад нарізки C ++ 11 - неправильний . Так само і з TBB, підказка - у замку, що охоплюється .
Джонатан Уейклі

3
Я добре знаю обох, @Jonathan. Ви ніби пропустили речення, яке я написав (could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer. Що стосується використання блокуваного блокування, це залежить від розробника, залежно від того, який саме додаток вони створюють. Ця відповідь мала на меті вирішити основне розуміння концепції мьютекс, а не вникати у всі її складності, тому ваші коментарі та посилання вітаються, але трохи виходять за рамки цього підручника.
Nav

41

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

//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 атомних інструкцій. У паралельній системі, де декілька потоків викликають функцію, проблеми виникають, коли нитка читає або записує в невідповідний час. Уявіть, що у нас є два потоки, які працюють simultaneoulsy, і один викликає функцію відразу за іншою. Скажімо також, що ми ініціалізували до 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

Що сталося, це те, що у нас збільшуються дві нитки i одночасно, наша функція викликається двічі, але результат не відповідає цьому факту. Схоже, функцію викликали лише один раз. Це тому, що атомність "порушена" на машинному рівні, тобто нитки можуть перебивати один одного або працювати разом у неправильний час.

Нам потрібен механізм для вирішення цього питання. Нам потрібно нав'язати деяке впорядкування до вищезазначених інструкцій. Один загальний механізм - блокувати всі потоки, крім одного. Pthread mutex використовує цей механізм.

Будь-який потік, який повинен виконувати деякі рядки коду, які можуть одночасно небезпечно змінювати спільні значення іншими потоками (використовуючи телефон для спілкування зі своєю дружиною), слід спочатку зробити замовлення на мьютекс. Таким чином, будь-який потік, який потребує доступу до спільних даних, повинен проходити через блокування mutex. Тільки тоді потік зможе виконати код. Цей розділ коду називають критичним розділом.

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

Концепція наявності мутексу здається дещо дивною, коли ми розглядаємо людей, які прагнуть виключного доступу до реальних фізичних об'єктів, але при програмуванні ми повинні бути навмисними. Одночасно нитки та процеси не мають соціального та культурного виховання, яке ми виконуємо, тому ми повинні змусити їх добре обмінюватися даними.

Отже, технічно кажучи, як працює мютекс? Чи не страждає він від тих самих умов гонки, про які ми згадували раніше? Чи не pthread_mutex_lock () трохи складніший, ніж простий приріст змінної?

Технічно кажучи, нам потрібна деяка апаратна підтримка, щоб допомогти нам. Конструктори апаратних засобів дають нам інструкції з машин, які роблять більше, ніж одне, але гарантовано є атомними. Класичним прикладом такої інструкції є тест-набір (TAS). Намагаючись придбати блокування ресурсу, ми можемо використовувати TAS, щоб перевірити, чи є значення в пам'яті 0. Якщо це так, це буде нашим сигналом про те, що ресурс використовується, і ми нічого не робимо (або більш точно , ми чекаємо деякого механізму. Mtex pthreads помістить нас у спеціальну чергу в операційній системі та повідомить нас, коли ресурс стане доступним. Дублюючі системи можуть вимагати від нас жорсткого циклу віджимання, перевіряючи стан знову і знову). . Якщо значення в пам'яті не дорівнює 0, TAS встановлює розташування на щось інше, ніж 0, не використовуючи жодних інших вказівок. Це ' подібне поєднанню двох інструкцій по збірці в 1, щоб надати нам атомність. Таким чином, тестування та зміна значення (якщо зміна доречна) не може бути перервана після її початку. На основі такої інструкції ми можемо будувати мутекси.

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


1
Дякую тобі, Сан. Я пов’язав вашу відповідь :) Насправді я мав намір прийняти мою відповідь + свою відповідь і опублікувати її як окрему відповідь, щоб утримати потік. Я не дуже проти, якщо ви повторно використовуєте будь-яку частину моєї відповіді. Ми все одно не робимо цього для себе.
Nav

13

Найкращий підручник з теми, про який я знаю, тут:

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

Мені подобається, що це написано про API, а не про певну реалізацію, і він дає кілька приємних простих прикладів, які допоможуть вам зрозуміти синхронізацію.


Я згоден, це, безумовно, хороший підручник, але це багато інформації на одній сторінці, а програми довгі. Питання, яке я розмістив, - це версія mutex мови "У мене є мрія", де новачки знайдуть простий спосіб дізнатися про мутекси та зрозуміти, як працює неінтуїтивний синтаксис (це одне пояснення, якого не вистачає у всіх навчальних посібниках) .
Nav

7

Нещодавно я наткнувся на цю публікацію і думаю, що їй потрібне оновлене рішення для стандартної бібліотеки 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;
}

1
Супер! Дякуємо за публікацію Хоча, як я вже згадував, моя мета полягала лише в тому, щоб просто пояснити поняття мютекс. У всіх інших навчальних посібниках було дуже складно з доданими поняттями споживача споживача та змінних умов тощо, що дуже ускладнило мені розуміння того, що відбувається.
Nav

4

Функція pthread_mutex_lock()або набуває mutex для виклику потоку, або блокує потік до тих пір, поки mutex не може бути придбаний. Пов'язане pthread_mutex_unlock()вивільняє мютекс.

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

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


1
Чи гарантовано, що саме наступний потік для спроб введеться?
Арсен Мкртчян

1
@Arsen Немає гарантії. Це просто корисна аналогія.
chrisaycock

3

Ви повинні перевірити змінну mutex, перш ніж використовувати область, захищену mutex. Тож ваш pthread_mutex_lock () міг (залежно від реалізації) зачекати, поки mutex1 буде випущений або поверне значення, що вказує на те, що блокування не вдалося отримати, якщо хтось уже заблокував його.

Mutex - це справді просто спрощений семафор. Якщо ви читаєте про них і розумієте їх, ви розумієте мютекси. Існує кілька питань щодо мутексів і семафорів в SO. Різниця між бінарним семафором і мютекс , коли ми повинні використовувати мутекс і коли ми повинні використовувати семафор тощо. Приклад туалету в першому посиланні є настільки ж хорошим прикладом, як можна придумати. Все, що потрібно зробити - це перевірити, чи є ключ доступним, і якщо він є, зарезервував його. Зверніть увагу, що ви не дуже резервуєте сам туалет, а ключ.


1
pthread_mutex_lockне може повернутися, якщо хтось інший тримає замок. Він блокує в цьому випадку і в цьому вся суть. pthread_mutex_trylockце функція, яка повернеться, якщо блокування утримується.
R .. GitHub СТОП ДОПОМОГА ВІД

1
Так, я спочатку не зрозумів, що це за реалізація.
Макіс

3

Для тих, хто шукає приклад шорт-файлу:

#include <mutex>

int main() {
    std::mutex m;

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.