5.3.2. М'ютекси
Поняття м'ютекса багато в чому збігається з поняттям блокування, визначеним у розділі 5.2. М'ютексом називають синхронізаційний примітив, що не допускає виконання деякого фрагмента коду більш як одним потоком. Фактично м'ютекс є реалізацією блокування на рівні ОС.
М'ютекс, як і випливає з його назви, реалізує взаємне виключення, його основне завдання - блокувати всі потоки, які намагаються отримати доступ до колу, коли цей код уже виконує деякий потік.
М'ютекс може перебувати у двох станах: вільному і зайнятому. Початковим станом є «вільний». Над м'ютексом можливі дві атомарні операції.
♦ Зайняти м'ютекс (mutex_lock): якщо м'ютекс був вільний, він стає зайнятим, і потік продовжує своє виконання (входячи у критичну секцію); якщо м'ютекс був зайнятий, потік переходить у стан очікування (кажуть, що потік «очікує на м'ютексі», або «заблокований па м'ютексі»), виконання продовжує інший потік. Потік, який зайняв м'ютекс, називають власникам м'ютекса (mutex owner):
mutex_lock (mutex_t mutex) {
if (mutex.state == free) {
mutex.state = loked;
mutex.owner = this_thread;
}
else sleep();
}
♦ Звільнити м'ютекс (mutex_unlock): м'ютекс стає вільним; якщо на ньому очікують кілька потоків, з них вибирають один, він починає виконуватися, займає м'ютекс і входить у критичну секцію. У більшості реалізацій вибір потоку буде випадковим. Звільнити м'ютекс може тільки його власник. Ось псевдокод цієї операції:
mutex_unlock (mutex_t mutex) {
if (mutex.owner != this_thread) return error:
mutex.state = free;
if (waiting_threads()) wakeup (some_thread):
}
Деякі реалізації надають ще третю операцію: спробувати зайняти м'ютекс (mutex_trylock): якщо м'ютекс вільний, діяти аналогічно до mutex_lock, якщо зайнятий - негайно повернути помилку і продовжити виконання.
Ось найпростіша реалізація критичної секції за допомогою м'ютекса.
mutex_t mutex:
mutex_lock(mutex):
// критична секція
mutex_unlock(mutex):
Основною відмінністю м'ютексів від двійкових семафорів (семафори цього виду ми використовували для блокування), є те, то звільнити м'ютекс може тільки його власник, тоді як змінити значення семафора може будь-який потік, котрий має до нього доступ. Ця відмінність досить суттєва і робить реалізацію взаємних виключень за допомогою м'ютексів простішою (з коду завжди ясно, яким потік може змінити стан м'ютекса).
Правила спрощеного паралелізму
Правила спрощеною паралелізму (easy concurrency rules) [78] призначені для спрощення програмування на базі м'ютексів. Вони ґрунтуються на тому очевидному факті, що м'ютекс захищає не код критичної секції, а спільно використовувані дані всередині цієї секції.
♦ Кожна змінна, яку спільно використовує більш як один потік, мас бути захищена окремим м'ютексом (скільки змінних, стільки м'ютексів):
volatile int i. data[100]:
mutex_t i_mutex. data_mutex:
♦ Перед кожною операцією зміни такої змінної відповідний м'ютекс мас бути зайнятий, а після зміни звільнений:
mutex_lock(i_ mutex): i++; mutex_unlock(i_ mutex):
♦ Якщо треба працювати одночасно із кількома спільно використовуваними змінними, необхідно зайняти всі їхні м'ютекси до початку роботи і звільнити їх тільки після повного закінчення роботи. Цим роботу розділяють на три етапи:
// зайняття м’ютексів
mutex_lock(i_ mutex): mutex_lock(data_ mutex):
// робота зі змінними
data[i++] = 100:
// звільнення м’ютексів
mutex_unlock(i_ mutex): mutex_unlock(data_ mutex):
Зазначимо, що для спільно використовуваних даних задано клас пам'яті volatile. Використання такого модифікатора сповіщає компілятор мови С, про можливе асинхронне змінення змінної поза даною програмою. Це завжди потрібно робити для спільно використовуваних даних.