Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекція № 5_МЗКІТ (10 ТП).docx
Скачиваний:
1
Добавлен:
01.07.2025
Размер:
4 Мб
Скачать
☆

5.3.2. М’ютекси

Поняття м’ютекса багато в чому збігається з поняттям блокування, визначеним у розділі 5.2. М’ютексом називають синхронізаційний примітив, що не допускає виконання деякого фрагмента коду більш як одним потоком. Фактично м’ютекс є реалізацією блокування на рівні ОС.

М’ютекс, як і випливає з його назви, реалізує взаємне виключення. Його основне завдання - блокувати всі потоки, які намагаються отримати доступ до коду, коли цей код уже виконує деякий потік.

М’ютекс може перебувати у двох станах: вільному і зайнятому. Початковим станом є «вільний». Над м’ютексом можливі дві атомарні операції.

♦ Зайняти м’ютекс (mutex_lock): якщо м’ютекс був вільний, він стає зайнятим, і потік продовжує своє виконання (входячи у критичну секцію); якщо м’ютекс був зайнятий, потік переходить у стан очікування (кажуть, що потік «очікує на м’ютексі», або «заблокований на м’ютексі»), виконання продовжує інший потік. Потік, який зайняв м’ютекс, називають власником м'ютекса (mutex owner):

♦ Звільнити м’ютекс (mutex_unl ock): м’ютекс стає вільним; якщо на ньому очікують кілька потоків, з них вибирають один, він починає виконуватися, займає м’ютекс і входить у критичну секцію. У більшості реалізацій вибір потоку буде випадковим. Звільнити м’ютекс може тільки його власник. Ось псевдокод цієї операції:

Деякі реалізації надають ще третю операцію; спробувати затіяти м’ютекс (mutex_trylock): якщо м’ютекс вільний, діяти аналогічно до mutexjock, якщо зайнятий — негайно повернути помилку і продовжити виконання.

Ось найпростіша реалізація критичної секції за допомогою м’ютекса.

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

Правила спрощеного паралелізму

Правила спрощеного паралелізму (easy concurrency rules) призначені для спрощення програмування на базі м’ютексів. Вони ґрунтуються на тому очевидному факті, що м’ютекс захищає не код критичної секції, а спільно використовувані дані всередині цієї секції.

♦ Кожна змінна, яку спільно використовує більш як один потік, має бути захищена окремим м’ютексом (скільки змінних, стільки м’ютексів):

♦ Перед кожною операцією зміни такої змінної відповідний м’ютекс має бути зайнятий, а після зміни звільнений:

♦ Якщо треба працювати одночасно із кількома спільно використовуваними змінними, необхідно зайняти всі їхні м’ютекси до початку роботи і звільнити їх тільки після повного закінчення роботи. Цим роботу розділяють на три етапи:

Зазначимо, що для спільно використовуваних даних задано клас пам’яті volаtile. Використання такого модифікатора сповіщає компілятор мови С, про можливе асинхронне змінення змінної поза даною програмою. Це завжди потрібно робити для спільно використовуваних даних.

Реалізація задачі виробників-споживачів за допомогою м’ютексів

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

Проблем із реалізацією критичних секцій не виникне, оскільки в цьому разі м’ютекси використовуватимуться за прямим призначенням.

Розглянемо організацію очікування виконання умови. Ця задача досить складна, оскільки м’ютекс, з одного боку, має тільки два стани, а з іншого - не може бути зайнятий в одному потоці, а звільнений в іншому. Це змушує вдатися до активного очікування, перевіряючи в циклі, чи не стала умова істинною (при ньому займаючи м’ютекс на час перевірки і звільняючи після неї). Ось як можна реалізувати функцію consumer( ):

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

Рекурсивні м’ютекси

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

Рекурсивний м'ютекс — особливий вид м’ютекса. Він дозволяє повторне зайняття тим самим потоком, а також відстежує, який потік намагається його зайняти.

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

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

Реалізація м’ютексів у POSIX

М’ютекси у POSIX зображаються структурами даних, що мають тип pthread_mutex_t. Для ініціалізації м’ютекса найпростіше використати статичний ініціалізатор:

Для того щоб зайняти м’ютекс, передбачено функцію pthread_mutex_lock( ), а для звільнення — pthread_mutex_unlock( ). Ось приклад роботи із м’ютексом для потоків POSIX:

Неблокувальним аналогом функції pthreadjnutexlock( ) є pthread_mutex_trylock( ). У разі неможливості зайняти м'ютекс вона негайно повертає помилку з кодом EBUSY.