Рекурсивний блокування (Mutex) проти нерекурсивного блокування (Mutex)


183

POSIX дозволяє мютексам бути рекурсивними. Це означає, що однаковий потік може двічі заблокувати один і той самий файловий файл і не матиме тупикової ситуації. Звичайно, його також потрібно розблокувати двічі, інакше жодна інша нитка не зможе отримати мутекс. Не всі системи, що підтримують pthreads, також підтримують рекурсивні мютекси, але якщо вони хочуть відповідати POSIX, вони повинні .

Інші API (більш API API високого рівня) також зазвичай пропонують мутекси, які часто називають Locks. Деякі системи / мови (наприклад, Cocoa Objective-C) пропонують як рекурсивні, так і нерекурсивні мутекси. Деякі мови також пропонують лише ту чи іншу. Напр., У мутексах Java завжди рекурсивні (одна і та ж нитка може двічі "синхронізуватися" на одному об'єкті). Залежно від того, яку іншу функціональність потоку вони пропонують, відсутність рекурсивних мютексів може не бути проблемою, оскільки їх можна легко записати самостійно (я вже реалізував рекурсивні мютекси самостійно на основі більш простих операцій mutex / condition).

Що я насправді не розумію: для чого корисні нерекурсивні мютекси? Чому я хотів би мати тупик потоку, якщо він двічі блокує ту саму мутекс? Навіть мови високого рівня, які могли б цього уникнути (наприклад, тестування, якщо це буде тупиком та викиданням винятку, якщо він є), зазвичай цього не роблять. Вони натомість пустять нитку в глухий кут.

Це лише для тих випадків, коли я випадково заблокував його двічі і розблокував лише один раз, а у випадку рекурсивної мютексу було б важче знайти проблему, тож натомість у мене є тупик негайно, щоб побачити, де з’являється неправильний замок? Але хіба я не міг би зробити те саме, щоб лічильник блокування повернувся під час розблокування та в ситуації, коли я впевнений, що випустив останній замок, а лічильник не дорівнює нулю, я можу викинути виняток або зареєструвати проблему? Або є якісь більш корисні випадки використання нерекурсивних мютексів, які я не бачу? Або це може бути просто продуктивність, оскільки нерекурсивна мютекс може бути трохи швидшою, ніж рекурсивна? Однак я тестував це, і різниця насправді не така вже й велика.

Відповіді:


154

Різниця між рекурсивним і нерекурсивним мутексним зв'язком пов'язана з правом власності. У випадку рекурсивного мютексу ядро ​​має відслідковувати нитку, яка фактично отримала мутекс вперше, щоб він міг виявити різницю між рекурсією та іншим потоком, який повинен блокуватись. Як вказується ще одна відповідь, виникає питання про додаткові накладні витрати, що стосуються пам’яті для зберігання цього контексту, а також циклів, необхідних для його збереження.

Однак тут є і інші міркування.

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

Ще одна властивість, яка надходить із відчуттям власності на мютекс, - це здатність підтримувати пріоритетне успадкування. Оскільки ядро ​​може відслідковувати потік, що володіє mutex, а також ідентифікацію всіх блокаторів (ив), в потоковій системі з пріоритетом стає можливим ескалація пріоритету потоку, який в даний час володіє mutex, до пріоритету потоку з найвищим пріоритетом який зараз блокується на мютекс. Це успадкування запобігає проблемі інверсії пріоритету, яка може виникнути в таких випадках. (Зауважте, що не всі системи підтримують пріоритетне успадкування на таких мутексах, але це ще одна особливість, яка стає можливою через поняття власності).

Якщо ви посилаєтесь на класичне ядро ​​VxWorks RTOS, вони визначають три механізми:

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

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


9
ваше пояснення про нерекурсивний мютекс звучало скоріше як семафор. Мутекс (рекурсивний чи нерекурсивний) має поняття власності.
Джей Д

@JayD Це дуже заплутано, коли люди сперечаються про подібні речі .. тож хто є суб'єктом, який визначає ці речі?
Pacerier

13
@Pacerier Відповідний стандарт. Ця відповідь, наприклад, неправильна для posix (pthreads), коли розблокування нормального мютексу в потоці, відмінному від потоку, який його заблокував, не визначене, при цьому те саме, що перевіряє помилку або рекурсивний мютекс, призводить до передбачуваного коду помилки. Інші системи та стандарти можуть поводитись дуже різними.
Н.З.К.

Можливо, це наївно, але в мене було враження, що центральна ідея мютексу полягає в тому, що фіксуюча нитка розблоковує мютекс, і тоді інші потоки можуть робити те саме. З computing.llnl.gov/tutorials/pthreads :
користувач657862

2
@curiousguy - випуск трансляції звільняє будь-які і всі потоки, заблоковані на семафорі, не даючи його явно (залишається порожнім), тоді як звичайний бінарний файл звільняє лише потік у голові черги очікування (якщо припустити, що він заблокований).
Високий Джефф

123

Відповідь - не ефективність. Невідповідні мутекси призводять до кращого коду.

Приклад: A :: foo () отримує замок. Потім він викликає B :: bar (). Це добре спрацювало, коли ти це написав. Але десь пізніше хтось змінює B :: bar () на виклик A :: baz (), який також отримує замок.

Ну, якщо у вас немає рекурсивних файлів, це глухі місця. Якщо у вас їх є, він працює, але він може зламатися. A :: foo (), можливо, залишив об'єкт у непослідовному стані перед викликом бар (), за умови, що baz () не може запуститися, оскільки він також набуває мьютекс. Але це, мабуть, не повинно бігати! Людина, яка написала A :: foo (), припускала, що ніхто не може одночасно викликати A :: baz () - ось у чому вся причина того, що обидва ці методи придбали замок.

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

Якщо ви задоволені замикаючими замками, це лише тому, що вам не довелося налагоджувати подібну проблему раніше. На сьогодні Java має незареєстровані блокування у java.util.concurrent.locks.


4
Мені знадобилось певний час, щоб зрозуміти, що ти говорив про інваріант, що не відповідає дійсності, коли ти схопиш замок вдруге. Гарна думка! Що робити, якщо це було блокування читання-запису (як-от Java ReadWriteLock), і ви придбали блокування читання, а потім повторно придбали замок читання вдруге в тій самій нитці. Ви не визнали б інвалідом інваліда після отримання права блокування читання? Отож, коли ви придбаєте замок для другого читання, інваріант все ще відповідає дійсності.
дгрант

1
@Jonathan Чи у Java в даний час є незареєстровані блокування у java.util.concurrent.locks ??
користувач454322

1
+1 Я здогадуюсь, що найпоширеніше використання для блокування повторного замовлення - це всередині одного класу, де деякі методи можна викликати як із захищених, так і з незахищених фрагментів коду. Це насправді завжди можна визначити. @ User454322 Звичайно, Semaphore.
maaartinus

1
Вибачте моє непорозуміння, але я не бачу, як це стосується мютексу. Припустимо, що тут не задіяно багатопотокове чи блокування, A::foo()можливо , він все ще залишив об'єкт у непослідовному стані перед викликом A::bar(). Що стосується мутексу, рекурсивного чи ні, пов'язаного з цією справою?
Сіюань Рен

1
@SiyuanRen: Проблема може міркувати локально про код. Люди (принаймні я) навчаються розпізнавати заблоковані регіони як інваріантне збереження, тобто під час придбання блокування жодна інша нитка не змінює стан, тому інваріанти в критичній області тримаються. Це не складне правило, і ви можете кодувати, коли інваріанти не мають на увазі, але це просто зробить ваш код складніше міркувати та підтримувати. Те саме відбувається в режимі одиночної різьби без м ясів, але там ми не навчені міркувати локально навколо захищеної області.
Девід Родрігес - дрибес

92

Як написав сам Дейв Бутенхоф :

"Найбільша з усіх великих проблем з рекурсивними мютексами полягає в тому, що вони спонукають вас повністю втратити свою схему та сферу блокування. Це смертельно. Зло. Це" пожирач ниток ". Ви тримаєте замки абсолютно короткий можливий час. Період завжди. Якщо ви дзвоните чомусь із замком, який тримається просто тому, що ви не знаєте, що він утримується, або тому, що ви не знаєте, чи потрібен виклик mutex, тоді ви занадто довго тримаєте його. націливши рушницю на вашу програму та натиснувши на спусковий гачок. Ви, ймовірно, почали використовувати нитки, щоб отримати сумісність, але ви просто ПЕРЕВІТИ паралельність ".


9
Також зверніть увагу на заключну частину відповіді ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
Бутенгофа

2
Він також розповідає, що використовувати єдиний глобальний рекурсивний мютекс (його думка полягає в тому, що вам потрібен лише один), це нормально як милиця, щоб свідомо відкласти важку роботу з розуміння інваріантності зовнішньої бібліотеки, коли ви починаєте використовувати її у багатопотоковому коді. Але ви не повинні використовувати милиці назавжди, а врешті вкладіть час, щоб зрозуміти та виправити інваріанти одночасності коду. Отже, ми можемо перефразовувати, що використання рекурсивного мютексу - це технічний борг.
FooF

13

Правильна ментальна модель використання мутексів: мютекс захищає інваріант.

Чому ви впевнені, що це дійсно правильна ментальна модель для використання мютексів? Я думаю, що правильна модель захищає дані, але не інваріанти.

Проблема захисту інваріантів існує навіть в однопотокових програмах і не має нічого спільного з багатопотоковими і мютексними текстами.

Крім того, якщо вам потрібно захистити інваріанти, ви все одно можете використовувати двійковий семафор, який ніколи не є рекурсивним.


Правда. Існують кращі механізми захисту інваріанта.
ActiveTrayPrntrTagDataStrDrvr

8
Це має бути коментарем до відповіді, яка запропонувала цю заяву. Мутекси не тільки захищають дані, вони також захищають інваріанти. Спробуйте написати якийсь простий контейнер (найпростіший - стек) з точки зору атоміки (де дані захищають себе) замість мутексів, і ви зрозумієте твердження.
Девід Родрігес - дрибес

Мутекси не захищають дані, вони захищають інваріант. Цей інваріант може бути використаний для захисту даних.
Джон Ханна

4

Однією з головних причин того, що рекурсивні мютекси корисні, є у випадку отримання доступу до методів кілька разів однією і тією ж ниткою. Наприклад, скажімо, що якщо блокування mutex захищає банк A / c відкликати, а якщо є також плата, пов’язана з цим зняттям, тоді повинен використовуватися той самий мютекс.


4

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

Якщо в методах використовуються інші методи (наприклад, addNewArray () викликає addNewPoint (), і завершує за допомогою повторної перевірки ()), але будь-яка з цих функцій сама по собі потребує блокування мютексу, тоді рекурсивний мютекс - це виграш.

Для будь-якого іншого випадку (вирішення просто поганого кодування, використання його навіть у різних об'єктах) явно неправильно!


1

Для чого корисні нерекурсивні мютекси?

Вони абсолютно хороші, коли вам потрібно переконатися, що мютекс розблокований, перш ніж щось робити. Це тому, що pthread_mutex_unlockможе гарантувати, що мютекс розблокований, лише якщо він не є рекурсивним.

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

Якщо g_mutexне є рекурсивним, код вище гарантовано дзвонить bar()із розблокованою мютексом .

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

Якби g_mutexбуло рекурсивно, просто не було б способу переконатися, що він розблокований, перш ніж здійснити дзвінок.

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