- •Урок № 61
- •Урок № 62
- •Урок № 72
- •Урок № 73
- •Створення зв'язку з файлом /dev/modem за допомогою команди lisa.
- •Настройка системи Linux для встановлення зв'язку.
- •Як створюється зв'язок з файлом /dev/modem за допомогою команди lisa?
- •Яким чином можна настроїти систему Linux для встановлення зв'язку?
- •Урок № 74
- •Урок № 75
- •Використання програми mail.
- •Урок № 76
- •Використання клієнта kpackage
- •Як використовується клієнт kpackage?
- •Урок № 77
- •Урок № 80
- •Урок № 82
- •Урок № 84
- •Урок № 85
- •Урок № 86
- •Урок № 87
- •Урок № 88
- •Урок № 89
- •Урок № 90
- •Урок № 92
- •Урок № 93
- •Урок № 95
- •Урок № 96
- •Урок № 97
- •Урок № 97
Урок № 96
(згідно навчальної робочої програми)
Тема: «Базові механізми синхронізації потоків»
Питання:
Поняття синхронізації.
Семафори.
Проблеми використання семафорів.
М’ютекси.
Правила спрощеного паралелізму.
Рекурсивні м’ютекси.
Умовні змінні та концепція монітора.
Відмінності умовних змінних від семафорів.
Рекурсивні м’ютекси й умовні змінні.
Очікування виконання кількох умов.
Поняття монітора.
Блокування читання-записування.
Типи блокувань.
Операції блокувань.
Синхронізація за принципом бар’єра.
Механізмами синхронізації є засоби операційної системи, які допомагають розв’язувати основне завдання синхронізації — забезпечувати координацію потоків, котрі працюють зі спільно використовуваними даними. Якщо такі засоби — це мінімальні блоки для побудови багатопотокових програм, їх називають синхронізаційними примітивами.
Синхронізаційні механізми поділяють на такі основні категорії:
універсальні, низького рівня, які можна використовувати різними способами (семафори);
прості, низького рівня, кожен з яких пристосований до розв’язання тільки однієї задачі (м’ютекси та умовні змінні);
універсальні високого рівня, виражені через прості; до цієї групи належить концепція монітора, яка може бути виражена через м’ютекси та умовні змінні;
високого рівня, пристосовані до розв’язання конкретної синхронізаційної задачі (блокування читання-записування і бар’єри).
Семафори
Семафор — це спільно використовуваний невід’ємний цілочисловий лічильник, для якого задано початкове значення і визначено такі атомарні операції.
Зменшення семафора(down): якщо значення семафора більше від нуля, його зменшують на одиницю, якщо ж значення дорівнює нулю, цей потік переходить у стан очікування доти, поки воно не стане більше від нуля. Цю операцію називають також очікуванням —wait. Ось її псевдокод:
voiddown (semaphore_tsem) { if (sem> 0) sem--; elsesleep():}
Збільшення семафора(up): значення семафора збільшують на одиницю; коли при цьому є потоки, які очікують на семафорі, один із них виходить із очікування і виконує свою операцію down. Якщо на семафорі очікують кілька потоків, то внаслідок виконання операції upйого значення залишається нульовим, але один із потоків продовжує виконання (у більшості реалізацій вибір цього потоку буде випадковим). Цю операцію також називають сигналізацією — post. Ось її псевдокод:
void up (semaphore_tsem) { sem++;
if (waiting_threads()) wakeup (some_thread);
}
Фактично значення семафора визначає кількість потоків, що може пройти через цей семафор без блокування. Коли для семафора задане нульове початкове значення, то він блокуватиме всі потоки доти, поки якийсь потік його не «відкриє», виконавши операцію up. Операції up і down можуть бути виконані будь-якими потоками, що мають доступ до семафора.
Семафори можна використовувати для розв’язання двох різних задач синхронізації.
За їхньою допомогою можна організовувати взаємне виключення. Найзручніше для цього використати двійковий семафор, який може приймати два значення: 0 і 1.
Проблеми використання семафорів
Насправді така універсальність має свій зворотний бік: код із використанням семафорів виходить досить складним.
Далеко не завжди просто відстежити, де і як може змінитися значення семафора.
Оскільки всі синхронізаційні дії реалізовані семафорами, програму потрібно добре документувати, щоб було ясно, який семафор за що відповідає.
У стані, коли обидва потоки залишаються — це взаємне блокування(deadlock), коли кожен із потоків очікує на блокуванні, запровадженим іншим.
Для розв’язання деяких із цих проблем було запропоновано розділити дві функції семафорів, надавши кожній із них окремий синхронізаційний примітив. Для організації взаємного виключення використовують м’ютекси, очікування виконання умови — умовні змінні.
М’ютекси
М’ютексом називають синхронізаційний примітив, що не допускає виконання деякого фрагмента коду більш як одним потоком. Фактично м’ютекс є реалізацією блокування на рівні ОС.
М’ютекс, як і випливає з його назви, реалізує взаємне виключення. Його основне завдання — блокувати всі потоки, які намагаються отримати доступ до коду, коли цей код уже виконує деякий потік.
М’ютекс може перебувати у двох станах: вільному і зайнятому. Початковим станом є «вільний». Над м’ютексом можливі дві атомарні операції.
Зайняти м’ютекс(mutex_lоск): якщо м’ютекс був вільний, він стає зайнятим, і потік продовжує своє виконання (входячи у критичну секцію); якщо м’ютекс був зайнятий, потік переходить у стан очікування, виконання продовжує інший потік. Потік, який зайняв м’ютекс, називають власником м’ютекса(mutex owner).
Звільнити м’ютекс(mutex_unlock): м’ютекс стає вільним; якщо на ньому очікують кілька потоків, з них вибирають один, він починає виконуватися, займає м’ютекс і входить у критичну секцію. У більшості реалізацій вибір потоку буде випадковим. Звільнити м’ютекс може тільки його власник.
Деякі реалізації надають ще третю операцію: спробувати зайняти м’ютекс(mutex_trylock): якщо м’ютекс вільний, діяти аналогічно до mutexjock, якщо зайнятий — негайно повернути помилку і продовжити виконання.
Основною відмінністю м’ютексів від двійкових семафорів є те, що звільнити м’ютекс може тільки його власник, тоді як змінити значення семафора може будь-який потік, котрий має до нього доступ. Ця відмінність досить суттєва і робить реалізацію взаємних виключень за допомогою м’ютексів простішою.
Правила спрощеного паралелізму
Правила спрощеного паралелізму(easy concurrency rules) призначені для спрощення програмування на базі м’ютексів. Вони ґрунтуються на тому очевидному факті, що м’ютекс захищає не код критичної секції, а спільно використовувані дані всередині цієї секції.
Кожна змінна, яку спільно використовує більш як один потік, має бути захищена окремим м’ютексом (скільки змінних, стільки м’ютексів):
Перед кожною операцією зміни такої змінної відповідний м’ютекс має бути зайнятий, а після зміни звільнений:
mutex_lock(i_mutex): і++; mutex_un1ock(i_mutex);
Якщо треба працювати одночасно із кількома спільно використовуваними змінними, необхідно зайняти всі їхні м’ютекси до початку роботи і звільнити їх тільки після повного закінчення роботи. Цим роботу розділяють на три етапи:
// зайняття м'ютексів
// робота зі змінними
// звільнення м'ютексів
Рекурсивні м’ютекси
Рекурсивний м’ютекс - особливий вид м’ютекса. Він дозволяє повторне зайняття тим самим потоком, а також відстежує, який потік намагається його зайняти.
Коли це не той потік, що його вже займає, м’ютекс поводиться як звичайний, і потік переходить у стан очікування. Коли ж це той самий потік, внутрішній лічильник блокувань цього м’ютекса збільшують на одиницю, і потік продовжує своє виконання. У разі звільнення м’ютекса внутрішній лічильник зменшують на одиницю, для інших потоків рекурсивний м’ютекс буде розблоковано тільки тоді, коли лічильник дійде до нуля.
Умовні змінні та концепція монітора
Умовною змінною називають синхронізаційний примітив, який дає змогу організувати очікування виконання умови всередині критичної секції, заданої м’ютексом. Умовна змінна завжди пов’язана із конкретним м’ютексом і даними, захищеними цим м’ютексом. Для умовної змінної визначено такі операції.
Очікування(wait).
Сигналізація(signal).
Широкомовна сигналізація(broadcast) відрізняється від звичайної тим, що переведення у стан готовності і, зрештою, поновлення виконують для всіх потоків, які очікують на цій умовній змінній, а не тільки для одного з них.
Отже, виконання операції wait складається з таких етапів: відкриття м’ютекса, очікування (поки інший потік не виконає операцію signalабо broadcast), закриття м’ютекса.
Відмінності умовних змінних від семафорів
Наведемо принципові відмінності умовних змінних від семафорів, які використовують для організації очікування за умовою.
Умовні змінні можуть бути використані лише всередині критичних секцій, при цьому інші потоки можуть входити у критичну секцію під час очікування на умовній змінній. Семафори небезпечно використовувати всередині критичних секцій, оскільки це зазвичай призводить до взаємного блокування.
Умовні змінні не зберігають стану, а семафори — зберігають.
Рекурсивні м’ютекси й умовні змінні
Рекурсивні м’ютекси не можуть бути використані у поєднанні з умовними змінними, оскільки рекурсивний м’ютекс може не звільнитися разом із початком очікування всередині wait, якщо він перед цим був зайнятий кілька разів (а це гарантує взаємне блокування).
Очікування виконання кількох умов
Можна організовувати очікування виконання кількох умов, хоча це рекомендують робити тільки у разі необхідності, щоб не ускладнювати код. Для такого очікування потрібно використати одну умовну змінну й у циклі whiІе перевіряти виконання кількох умов/
Поняття монітора
Монітором називають набір функцій, які використовують один загальний м’ютекс і нуль або більше умовних змінних для керування паралельним доступом до спільно використовуваних даних відповідно до певних правил. Функції цього набору називають функціями монітора.
Ось правила, яких слід дотримуватися у разі реалізації монітора.
Під час входу в кожну функцію монітора потрібно займати м’ютекс, під час виходу — звільняти. Отже, у кожний момент часу тільки один потік може перебувати всередині монітор.
Під час роботи з умовною змінною (і під час очікування, і під час сигналізації) необхідно завжди вказувати відповідний м’ютекс. Не можна працювати з умовними змінними, якщо м’ютекс незайнятий.
Під час перевірки на виконання умови очікування потрібно використати цикл, а не умовний оператор.
Блокування читання-записування
М’ютекси є засобом, який захищає спільно використовувані дані від будь-якого одночасного доступу з боку кількох потоків — будь то читання чи зміна.
Насправді нам не завжди потрібен такий однозначний захист, наприклад, для певного типу задач хотілося б розрізняти читання спільно використовуваних даних та їхню модифікацію (для того, щоб, скажімо, дозволяти читання кільком потокам одночасно, а модифікацію - тільки одному). Для розв’язання такої задачі використовують блокування читання-записування(read-writelocks).
Блокування читання-записування — це синхронізаційний примітив, для якого визначені два режими використання: відкриття для читання і відкриття для записування.
Насправді нам не завжди потрібен такий однозначний захист, наприклад, для певного типу задач хотілося б розрізняти читання спільно використовуваних даних та їхню модифікацію. Для розв’язання такої задачі використовують блокування читання-записування(read-writelocks).
Простіше кажучи, читати дані може будь-яка кількість потоків одночасно за умови, що ніхто ці дані не змінює; змінювати дані можна тільки тоді, коли їх ніхто не читає і не змінює.
Типи блокувань
Розрізняють два типи блокувань читання-записування: з кращим читанням і з кращим записом. Відмінність між ними виявляється тоді, коли потік намагається відкрити таке блокування для читання за умови, що він робить це не першим і що є призупинені потоки, які очікують можливості відкрити це блокування для записування.
У разі кращого читання потік негайно відкриває блокування для читання і продовжує свою роботу незалежно від того, є потоки-записувачі, що очікують, чи ні. Потоки-записувачі продовжують своє очікування.
У разі кращого записування за наявності потоків-записувачів, що очікують, потік-читач припиняється й не буде поновлений доти, поки всі записувачі не виконають свої дії і не закриють блокування.
Lля обох типів потік-записувач не може відкрити блокування, поки його тримає відкритим хоча б один читач, — перевага надається тільки новим потокам-читачам, які намагаються відкривати додаткові блокування.
Операції блокувань
Розглянемо операції, допустимі для блокувань читання-записування.
Відкриття для читання(rwlock_rdlock). Якщо є потік, який відкрив блокування для записування, поточний потік припиняють. Якщо такий потік відсутній:
у разі кращого читання блокування відкривають для читання і потік продовжує своє виконання;
у разі кращого записування перевіряють, чи немає призупинених потоків, які очікують відкриття цього блокування для записування; якщо вони є - потік припиняють, якщо немає — блокування відкривають для читання і потік продовжує своє виконання.
При цьому необхідно, щоб кілька потоків могли відкрити блокування для читання, тому в разі, коли блокування вже відкрите для читання, для його нового відкриття збільшують внутрішній лічильник потоків-читачів.
Відкриття для записування(rwlock_wr1ock). Якщо є потік, який відкрив блокування для читання або записування, поточний потік припиняють; коли жодного такого потоку немає, блокування відкривають для записування і потік продовжує своє виконання.
Закриття(rwlock_unlock). У разі наявності кількох потоків, які відкрили блокування для читання, воно залишається відкритим для читання, і внутрішній лічильник потоків-читачів зменшують на одиницю.
Блокування читання-записування, як і м’ютекси, мають власника, тому не можна закрити блокування в потоці, який його не відкривав.
Синхронізація за принципом бар’єра
Іноді буває зручно розбити задачу на кілька послідовних етапів, при цьому виконання наступного етапу не може розпочатися без цілковитого завершення попереднього.
Бар’єр — це блокування, яке зберігають доти, поки кількість потоків, що очікують, не досягне деякого наперед заданого числа, після чого всі ці потоки продовжують своє виконання.
Так, якщо наприкінці етапу задачі поставити бар’єр, він буде утримувати потоки від продовження, поки вони всі не завершать роботу цього етапу.
Розглянемо дві операцій бар’єра.
Ініціалізація бар’єра(barriеr_іnit). Параметром даної операції є п — кількість потоків, які ми синхронізуватимемо на цьому бар’єрі. Внутрішній лічильник призупинених потоків покладають рівним нулю. Значення п після виклику barrier_init міняти не можна.
Очікування на бар’єрі(barrier_wait). Внаслідок даної операції лічильник призупинених потоків збільшується на одиницю. Якщо він не досягнув п, цей потік призупиняють. Якщо ж після збільшення лічильник виявляється рівним п (досягнуто максимальної кількості потоків для бар’єра), усі потоки, що очікували на бар’єрі, поновлюються (ситуація переходу бар’єра), при цьому одному з них повертають спеціальне значення (зазвичай -1), а іншим - нуль. Надалі потік, що отримав це спеціальне значення, може зробити деякі підсумкові дії після переходу бар’єра. При переході бар’єра його «скидають» (внутрішній лічильник призупинених потоків знову покладають рівним 0) і він починаючи із наступної операції barri er_wait блокуватиме потоки знову.
Контрольні питання:
В чому суть синхронізації?
Що таке семафори?
Які існують проблеми використання семафорів?
Що таке м’ютекси?
Які існують правила спрощеного паралелізму?
Що таке рекурсивні м’ютекси?
Як використовуються умовні змінні та концепція монітора?
В чому відмінності умовних змінних від семафорів?
Як співпрацюють рекурсивні м’ютекси й умовні змінні?
Для чого призначено очікування виконання кількох умов?
Що таке монітор?
Як реалізовано блокування читання-записуванн?
Які є типи блокувань?
Які існують операції блокуван?
Як здійснюється синхронізація за принципом бар’єра?
Література: Шеховцов В.А. Операційні системи. – К.: Видавнича група BHV, 2005. – 576 с.: іл., стор. 117-136.
