- •4.6.2. Програмний інтерфейс планування
- •4.1. Загальні принципи планування
- •4.1.1. Особливості виконання потоків
- •4.1.2. Механізми і політика планування
- •4.1.3. Застосовність принципів планування
- •4.2. Види планування
- •4.2.1. Довготермінове планування
- •4.3. Стратеги планування. Витісняльна і невитісняльна багатозадачність
- •4.2.2. Середньотермінове планування
- •4.2.3. Короткотермінове планування
- •4.4. Алгоритми планування
- •4.4.1. Планування за принципом fifo
- •4.4.2. Кругове планування
- •4.4.4. Планування на підставі характеристик подальшого виконання
- •4.4.5. Багаторівневі черги зі зворотним зв’язком
- •4.4.6. Лотерейне планування
- •4.5. Реалізація планування в Linux
- •4.5.1. Планування процесів реального часу в ядрі
- •4.5.2. Традиційний алгоритм планування
- •4.5.3. Сучасні підходи до реалізації планування
- •4.5.4. Програмний інтерфейс планування
- •4.6. Реалізація планування у Windows хр
- •4.6.1. Планування потоків у ядрі
- •4.6.2. Програмний інтерфейс планування
- •Висновки
- •5.1. Основні принципи взаємодії потоків
- •5.2. Основні проблеми взаємодії потоків
- •5.2.1. Проблема змагання
- •5.2.2. Критичні секції та блокування Поняття критичної секції
- •5.3. Базові механізми синхронізації потоків
- •5.3.1. Семафори
- •5.3.2. М’ютекси
- •5.3.3. Умовні змінні та концепція монітора
- •5.3.4. Блокування читання-записування
- •5.3.5. Синхронізація за принципом бар’єра
- •5.4. Взаємодія потоків у Linux
- •5.4.1. Механізми синхронізації ядра Linux
- •5.4.2. Синхронізація процесів користувача у Linux. Ф’ютекси
- •5.5. Взаємодія потоків у Windows хр
- •5.5.1. Механізми синхронізації потоків ос
- •5.5.2. Програмний інтерфейс взаємодії Win32 арі
- •Висновки
- •Контрольні запитання та завдання
5.3.4. Блокування читання-записування
М’ютекси є засобом, який захищає спільно використовувані дані від будь-якого одночасного доступу з боку кількох потоків — будь то читання чи зміна Насправді нам не завжди потрібен такий однозначний захист, наприклад, для певного типу задач хотілося б розрізняти читання спільно використовуваних даних та їхню модифікацію (для того, щоб, скажімо, дозволяти читання кільком потокам одночасно, а модифікацію — тільки одному). Для розв’язання такої задачі використовують блокування читання-записування (read-write locks).
Блокування читання-записування — це синхронізаційний примітив, для якого визначені два режими використання: відкриття для читання і відкриття для записування. При цьому повинні виконуватися такі умови:
♦ будь-яка кількість потоків може відкривати таке блокування для читання, коли немає жодного потоку, що відкрив його для записування;
♦ блокування може відкриватися для записування тільки за відсутності потоку, що відкрив його для читання або для записування.
Простіше кажучи, читати дані може будь-яка кількість потоків одночасно за умови, що ніхто ці дані не змінює; змінювати дані можна тільки тоді, коли їх ніхто не читає і не змінює.
Такі блокування корисні для даних, які зчитуються частіше, ніж модифікуються (наприклад, більшість СУБД реалізує блокування такого роду для забезпечення доступу до бази даних).
Типи блокувань
Розрізняють два тини блокувань читання-записування: з кращим читанням і з кращим записом. Відмінність між ними виявляється тоді, коли потік намагається відкрити таке блокування для читання за умови, що він робить це не першим і що є призупинені потоки, які очікують можливості відкрити це блокування для записування.
♦ У разі кращого читання потік негайно відкриває блокування для читання і продовжує свою роботу незалежно від того, є потоки-записувачі, що очікують, чи ні. Потоки-записувачі продовжують своє очікування.
♦ У разі кращого записування за наявності потоків-записувачів, що очікують, потік-читач припиняється й не буде поновлений доти, поки всі записувачі не виконають свої дії і не закриють блокування.
Зазначимо, що для обох типів потік-записувач не може відкрити блокування, поки його тримає відкритим хоча б один читач, — перевага надається тільки новим потокам-читачам, які намагаються відкривати додаткові блокування.
Операції блокувань
Розглянемо операції, допустимі для блокувань читання-записування.
♦ Відкриття для читання (rwlock_rdlock). Якщо є потік, який відкрив блокування для записування, поточний потік припиняють. Якщо такий потік відсутній:
у разі кращого читання блокування відкривають для читання і потік продовжує своє виконання;
у разі кращого записування перевіряють, чи немає призупинених потоків, які очікують відкриття цього блокування для записування; якщо вони є - потік припиняють, якщо немає -- блокування відкривають для читання і потік продовжує своє виконання.
При цьому необхідно, щоб кілька потоків могли відкрити блокування для читання, тому в разі, коли блокування вже відкрите для читання, для його нового відкриття збільшують внутрішній лічильник потоків-читачів.
♦ Відкриття для записування (rwlock_wrlock). Якщо є потік, який відкрив блокування для читання або записування, поточний потік припиняють; коли жодного такого потоку немає, блокування відкривають для записування і потік продовжує своє виконання.
♦ Закриття (rw1ock_unlock). У разі наявності кількох потоків, які відкрили блокування для читання, воно залишається відкритим для читання, і внутрішній лічильник потоків-читачів зменшують на одиницю. Якщо блокування відкрите для читання тільки одним потоком, (лічильник дорівнює одиниці) його знімають, якщо є потоки-записувачі, які очікують на цьому блокуванні, один із них поновлюють. Коли блокування відкрите для записування, його знімають, при цьому за наявності потоків-читачів, що очікують, всі вони поновлюються, а з потоків-записувачів, що очікують, поновлюють тільки один. Якщо очікують і читачі й записувачі, результат залежить від типу блокування (у разі кращого читання або якщо жодного записувача немає, поновлюють усіх читачів, а якщо читачі відсутні у разі кращого записування — поновлюють одного з записувачів.
Блокування читання-записування, як і м’ютекси, мають власника, тому не можна закрити блокування в потоці, який його не відкривав.
Ось псевдокод прикладу використання таких блокувань для синхронізації доступу до банківського рахунку:
Голодування
Під час використання блокувань читання-записування можуть виникати проблеми, пов'язані з типом блокування. Якщо система працює за умов значного навантаження, для блокувань із кращим читанням можлива така послідовність дій потоків.
1. Потік TR1 відкриває блокування для читання L.
2. Потік TW1 намагається відкрити блокування L для записування і призупиняється.
3. Потік ТR2 відкриває блокування L для читання.
4. Потік TR1 виконує операцію закриття блокування, але блокування L залишається зайнятим ТR2.
5. Потік TR3 відкриває блокування L для питання.
6. Потік ТR2 виконує операцію закриття блокування, але блокування L залишається зайнятим TR3 (і т. д.).
У результаті додаткові потоки-читачі захоплюватимуть блокування L до того, як його звільнять усі попередні читачі. Це означає, що L ніколи повністю не звільниться, і потік-записувач TW1 ніколи не отримає керування. Таку ситуацію, як і під час планування, називають голодуванням.
Для блокувань із кращим записуванням голодування потоку-записувача бути не може. У нашому випадку потік ТR2 на кроці 3 призупиниться і після закриття, блокування потоком. TR1 на кроці 4 потік TW1 дістане можливість виконуватися. З іншого боку, у цьому випадку за певних умов можна отримати голодування потоків-читачів.
Остаточного розв’язання проблеми голодування не запропоновано, рекомендують ретельно зважувати можливість використання різних типів блокувань і розраховувати можливість навантаження, яке може призвести до голодування.
Блокування читання-записування відповідно до POSIX
У літературі є багато прикладів реалізації блокувань читання-записування на базі інших примітивів, наприклад м’ютексів та умовних змінних. Останнім часом бібліотеки підтримки потоків (наприклад, NPTL у Linux) пропонують такі блокування як базові синхронізаційні примітиви; вони визначені стандартом POSIX (а нестандартні розширення дозволяють задавати і тип блокування).
Розглянемо реалізацію блокувань читання-записування відповідно до POSIX. Для роботи із такими блокуваннями у стандарті POSIX використовують тип pthread_rwlock_t. Усі функції роботи із блокуваннями як параметр приймають покажчик на змінну такого тішу. Найпростішу ініціалізацію блокування здійснюють за допомогою статичного ініціалізатора
Відкриття такого блокування для записування відбувається за допомогою функції pthread_rwlock_wrlock( ), відкриття для читання — за допомогою функції pthread_rwlock_rdlock( ).
Для закриття блокування використовують функцію pthread_rwlock_unlock( ): pthread_rwlock unlock(&rwlock);
