Ім'я файлу: Синхронізація потоків.doc
Розширення: doc
Розмір: 44кб.
Дата: 17.02.2023
скачати
Пов'язані файли:
ÊÐ ÒÅÑÒÓÂÀÍÍß copy.docx

Потоки. Синхронізація потоків


Для синхронізації дій, виконуваних різними потоками існує кілька різних способів. Умовно їх можна розділити на наступні:
· Синхронізація за допомогою глобальних змінних
· Синхронізація за допомогою обміну подіями
· Синхронізація за допомогою спеціальних об'єктів (подій, семафорів, критичних секцій, об'єктів виключного володіння та інших)
Перший метод синхронізації слід визнати найбільш невдалим, так як він вимагає постійного опитування стану глобальної змінної. У цього методу є й більш серйозні недоліки - так, наприклад, можлива повна блокування потоків, якщо потік, що очікує зміни глобальної змінної має більш високий пріоритет, ніж потік, що змінює цю змінну. Щоправда, його можна дещо поліпшити - вводячи додаткові тимчасові затримки між послідовними опитуваннями.
Другий метод вимагає створення об'єктів, здатних отримувати повідомлення або повідомлення про виконання деяких дій. Це можуть бути вікна, файли (наприклад, при використанні асинхронних операцій чи операцій з повідомленням), каталоги і т.д. У багатьох випадках може бути зручно створити невидиме вікно, бере участь в обміні повідомленнями чи очікує отримання повідомлень для виконання тих чи інших дій.
Третій метод - синхронізація за допомогою спеціальних об'єктів - зажадає розгляду різних об'єктів і різні методи синхронізації з використанням об'єктів. У Win32 API існує велика кількість об'єктів, які можуть бути застосовані в якості синхронизирующих, причому в багатьох випадках замість одного об'єкта може застосовуватися об'єкт іншого типу. Тому буде цікаво розглянути кілька основних методів синхронізації з використанням об'єктів.

Загальні уявлення про методи синхронізації


При розгляді засобів синхронізації зручно виділити кілька основних способів, що застосовуються різними операційними системами. У числі таких способів можна виділити чотири методи:
· Критичні секції
· Об'єкти виключного володіння
· Події
· Синхронізація групою об'єктів
Послідовно розглянемо ці основні методи, використовуючи як приклад функції по роботі з глобальною купою. Спробуємо абстрактно розібратися з властивостями цих функцій, що б зрозуміти, які методи синхронізації і в яких випадках зручно застосовувати.
По-перше, глобальну купу в цілому можна розглядати як якийсь складний об'єкт, над яким можуть виконуватися деякі операції (поки ми розглядаємо лише операції над купою в цілому, не вдаючись у нюанси роботи з окремими блоками - при необхідності доступ до окремих блоках купи повинен бути синхронізований, але це вже не функції купи, а проблема того, хто користується купою).
Частина операцій, наприклад одержання покажчика на якийсь блок в купі, не потребує зміни самої купи. Тобто функції, які здійснюють подібні операції можуть виконуватися одночасно різними потоками не конфліктуючи один з одним. Такі функції зручно назвати "читачами" - вони не змінюють купу як єдиний об'єкт.
Інші операції, наприклад виділення нового блоку в купі, вимагають внесення змін до купи - зміни в ланцюжку виділяються блоків. Зазвичай такі зміни здійснюються не в одному місці, а потрібно узгоджене внесення змін до декількох структурах даних. У процесі такої операції структура купи якийсь час виявляється порушеною, внаслідок чого одночасне виконання таких дій має бути виключено. Такі функції зручно назвати "письменниками" - вони змінюють купу як єдиний об'єкт.
Стосовно до купи можливий одночасний доступ декількох читачів і винятковий - письменників. Більш того, якщо до купи отримує доступ письменник, то читачі також повинні бути позбавлені доступу (в інших випадках це може бути не так - читачі можуть працювати одночасно з письменником).
Можна сформулювати декілька правил для роботи з таким об'єктом:
· Якщо до об'єкту має доступ письменник, то ні читачі, ні інші письменники доступу не мають;
· Якщо до об'єкту має доступ читач, то можливий одночасний доступ інших читачів і заборонений доступ письменникам;
· Якщо об'єкт вільний, то перший прийшов письменник чи читач має право доступу.
Розглянутий приклад дуже зручний, тому що подібна ситуація (багато читачів, єдиний письменник) зустрічається суцільно й поруч - а стандартного об'єкта для синхронізації доступу до такого об'єкта немає. Завдяки цьому такий приклад стає благодатним грунтом для розгляду різних способів синхронізації.

Номер

Потік, що вже має доступ

Потік, що вимагає доступу

Дія

1

читач

читач

дозволити доступ

2

читач

письменник

чекати звільнення [1]

3

письменник

читач

чекати звільнення [1]

4

письменник

письменник

чекати звільнення

5

-

читач

дозволити доступ

6

-

письменник

дозволити доступ

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

Критичні секції


Цей синхронізуючий об'єкт може використовуватися тільки локально всередині процесу, що створив його. Інші об'єкти можуть бути використані для синхронізації потоків різних процесів. Назва об'єкта "критична секція" пов'язане з деяким абстрактним виділенням частини програмного коду (секції), що виконує деякі операції, порядок яких не може бути порушений. Тобто спроба двома різними потоками одночасно виконувати код цієї секції призведе до помилки.
Наприклад, такий секцією може бути зручно захистити функції-письменники, так як одночасний доступ кількох письменників має бути виключений.
Для критичної секції вводять дві операції:
· Увійти в секцію;
Поки який-небудь потік знаходиться в критичній секції, всі інші потоки при спробі увійти в неї будуть автоматично зупинятися в очікуванні. Потік, вже увійшов в цю секцію, може входити в неї багато разів, не чекаючи її звільнення.
· Покинути секцію;
При покиданні потоком секції зменшується лічильник числа входжень цього потоку в секцію, так що секція буде звільнена для інших потоків тільки якщо потік вийде з секції стільки разів, скільки разів у неї входив. При звільненні критичної секції буде пробуджений тільки один потік, який чекає дозволу на вхід в цю секцію.
Взагалі кажучи, в інших API, відмінних від Win32 (наприклад, OS / 2), критична секція розглядається не як синхронізуючий об'єкт, а як фрагмент коду програми, який може виконуватися тільки одним потоком додатки. Тобто вхід в критичну секцію розглядається як тимчасове вимкнення механізму перемикання потоків до виходу з цієї секції. У Win32 API критичні секції розглядаються як об'єкти, що призводить до певної плутанини - вони дуже близькі за своїми властивостями до неіменованим об'єктів виключного володіння (mutex, див. нижче).
При використанні критичних секцій треба стежити, що б у секції не виділялися надто великі фрагменти коду, так як це може призвести до суттєвих затримок у виконанні інших потоків.
Наприклад, стосовно вже розглянутих купах - не має сенсу всі функції по роботі з купою захищати критичної секцією, тому що функції-читачі можуть виконуватися паралельно. Більше того, застосування критичної секції навіть для синхронізації письменників насправді видається малоудобним - тому що для синхронізації письменника з читачами останнім все-одно доведеться входити в цю секцію, що практично призводить до захисту всіх функцій єдиної секцією.
Можна виділити кілька випадків ефективного застосування критичних секцій:
· Читачі не конфліктують з письменниками (захищати треба тільки письменників);
· Всі потоки мають приблизно рівні права доступу (скажімо, не можна виділити чистих письменників і читачів);
· При побудові складових синхронизирующих об'єктів, що складаються з декількох стандартних, для захисту послідовних операцій над складеним об'єктом.

Об'єкти виключного володіння


Об'єкти виключного володіння - mutex (mut ual ex clusion) - можуть належати тільки одному потоку одночасно. Відповідно визначаються операції над цими об'єктами:
· Зажадати володіння об'єктом;
При запиті володіння система перевіряє, володіє-Чи є який-небудь інший потік цим об'єктом чи ні. Якщо є інший потік-власник, то даний потік зупиняється до тих пір, поки об'єкт не звільнитися. Як тільки об'єкт стає вільним, система віддає його у володіння новому потоку. Потік, що вже володіє об'єктом, може багаторазово вступати у володіння ім.
· Звільнити об'єкт;
При звільненні об'єкта система переглядає, є-ли інші потоки, які очікують звільнення цього об'єкту. Якщо є, то відновить роботу тільки один потік, а всі інші продовжать очікування - об'єкт mutex може бути у володінні тільки в одного потоку. Звільняти об'єкт може тільки той потік, який їм в даний момент володіє, інші потоки цього зробити не можуть. Для повного звільнення об'єкта потік повинен стільки разів звільнити його, скільки разів він зажадав володіння з того моменту, як йому дали цей об'єкт у володіння.
Якщо врахувати, що володіти об'єктом mutex може тільки один потік, то виходить, що такі об'єкти схожі на критичні секції - з того моменту, як система віддала об'єкт у володіння потоку всі інші, які захочуть отримати його у володіння, будуть очікувати його звільнення. Відмінності полягають у деяких нюансах використання - по-перше, об'єкти mutex можуть бути використані різними процесами (для цього передбачені іменовані об'єкти, які можуть бути використані іншими процесами) і, по-друге, очікувати володіння цим об'єктом можна різними способами - з обмеженням за часом або, наприклад, використовувати його для синхронізації відразу з декількома об'єктами (іншими об'єктами mutex, семафорами, подіями та іншим). Про це - детальніше в наступних розділах.

Події


Події, як і об'єкти виключного володіння, можуть використовуватися для синхронізації потоків, що належать різним додаткам. Найбільш значні відмінності зводяться до наступного:
· Подіями ніхто не володіє - тобто встановлювати події у вільний або зайняте стан можуть будь-які потоки, що мають право доступу до цього об'єкта
· Події розрізняють тільки два стани - вільне і зайняте. Скільки б разів ви не переводили подія у зайняте стан, один єдиний виклик функції, що звільняє цю подію, звільнить його. І навпаки.
· У класичному варіанті звільнення події дозволяє запуск всіх потоків, які чекають його. Об'єкти виключного володіння і критичні секції дозволяли відновити виконання тільки одного потоку [2].
· Зміна стану події здійснюється в будь-який момент часу. Так, для входження в критичну секцію або для отримання об'єкта mutex у володіння необхідно було дочекатися їх звільнення (що виконувалося автоматично). Для подій це не так.
Відповідно, стосовно подій, говорять про дві основні станах: вільному (встановленому) і зайнятому (скинутому) і про три операціях, виконуваних над ними:
· Скинути подія;
Подія в скинутому стані вважається зайнятим. Аналогія - прапорець таксі (у Росії - лампа зеленого кольору). Опущений прапор (або вимкнена лампа) означають, що таксі зайнято. Будь-який потік, що має доступ до події, може скинути його, незалежно від того, який потік це подія встановлював.
· Встановити (іноді - послати) подія;
Встановлене (послане) подія вважається вільним. Як тільки подія звільняється, всі очікують його потоки можуть відновити своє виконання (див. виноску). Встановлювати подія може також будь-який потік.
· Дочекатися події;
Так як скидання і установка подій відбувається в будь-який момент часу, незалежно від попереднього стану події, то доводиться вводити спеціальну операцію - дочекатися звільнення об'єкта.
Ці особливості виділяють події в особливу категорію синхронизирующих об'єктів. Розглянуті раніше критичні секції і об'єкти виключного володіння дозволяють здійснити монопольний доступ до якого-небудь ресурсу чи об'єкту даних, в той час як події визначають деякі умови, після яких можливе виконання тих чи інших операцій.

Приклад. Повертаючись до прикладу з глобальної купою - для роботи з нею необхідно створити складовою синхронізуючий об'єкт. Приклад такого об'єкта можна знайти в книзі "Windows для професіоналів" Джеффрі Ріхтера. Природно, логіка побудови об'єкта однакова і в даному випадку, що дозволяє детально порівняти кілька близьких рішень і загострити увагу на деяких "дрібниці". Зараз ми розглянемо тільки "скелет" такого об'єкта та основні правила роботи з ним. Пізніше, після розгляду функцій Win32 API, розглянемо конкретну реалізацію цього складного об'єкта і порівняємо її з надзвичайно схожим об'єктом, наведеним у книзі Ріхтера.


Спочатку спробуємо скласти уявлення про тих стандартних об'єктах, які треба використовувати для побудови складеного синхронізуючого об'єкта. Ми маємо справу з потоками-письменниками, мають винятковий доступ до даних, і потоками-читачами, які можуть мати одночасний доступ до тих-же даними. Проте наявність хоча-б одного читача виключає для письменників можливість доступу до даних.
З цих міркувань випливає наявність:
· Критичної секції або об'єкта виключного володіння для синхронізації потоків-письменників. Вибір одного з цих об'єктів визначається необхідністю виключного доступу тільки одного потоку-письменника до даних. Зручно також, що звільнити секцію або mutex може тільки той потік, який його зайняв. У розглянутому прикладі будемо використовувати об'єкт mutex, для більшої схожості з Ріхтером (це дозволить подчернуть кілька нюансів у розробці такого об'єкта).
· Події, що переходить у зайняте стан при наявності хоча б одного читача. У даному випадку доцільно вибрати подія, яка буде скидатися у зайняте стан при появі першого потоку-читача і встановлюватися у вільний останнім читачем (останнім з числа тих, хто намагається здійснити читання одночасно з іншими, але не взагалі останнього).
· Лічильника числа потоків-читачів, які здійснюють одночасний доступ. Нульове значення лічильника відповідає встановленому у вільний стан події. При збільшенні лічильника перевіряється його початкове значення, і якщо воно було 0, то подія скидається у зайняте стан. При зменшенні лічильника перевіряється результат - якщо він 0, то подія встановлюється у вільний стан.
Можна приблизно описати структуру такого об'єкта (назвемо його NEWSWMRG, в порівнянні з об'єктом SWMRG, що розглядаються у Ріхтера - S ingle W riter M ulti R eader G uard). Ця структура повинна бути описана приблизно так:
struct NEWSWMRG {
ПОДІЯ НЕТ_ЧІТАТЕЛЕЙ;
ЛІЧИЛЬНИК ЧІСЛО_ЧІТАТЕЛЕЙ;
ОБЪЕКТ_ИСКЛЮЧИТЕЛЬНОГО_ВЛАДЕНИЯ ЕСТЬ_ПІСАТЕЛІ;
};
Для роботи з нею треба буде виділити чотири спеціальні функції (ініціалізацію та видалення цього об'єкта поки не розглядаємо): дві функції, які використовуються потоками-читачами для отримання доступу до даних і для позначення кінця операції читання, а також дві аналогічні функції для потоків-письменників.
void RequestWriter (NEWSWMRG * p) {
/ / Дочекатися дозволу для письменника
Захопити об'єкт ЕСТЬ_ПІСАТЕЛІ;
/ / Якщо об'єкт отриманий у володіння - інших письменників більше немає
/ / І поки ми його не звільнимо - не з'являться.
Дочекатися події НЕТ_ЧІТАТЕЛЕЙ;
/ / Якщо подія встановлено - читачів також немає
}
void ReleaseWriter (NEWSWMRG * p) {
/ / Після зміни даних дозволяємо доступ іншим письменникам і читачам
Звільнити об'єкт ЕСТЬ_ПІСАТЕЛІ;
}
void RequestReader (NEWSWMRG * p) {
/ / Дочекатися дозволу для читача - переконатися, що немає письменників
/ / Для цього можна захопити об'єкт ЕСТЬ_ПІСАТЕЛІ і відразу звільнити його
/ / Захоплення пройде тільки тоді, коли письменників немає.
Захопити об'єкт ЕСТЬ_ПІСАТЕЛІ;
/ / Реально треба не тільки переконатися у відсутності письменників, а й
/ / Збільшити лічильник і при необхідності скинути подія НЕТ_ЧІТАТЕЛЕЙ
if (ЧІСЛО_ЧІТАТЕЛЕЙ == 0) Скинути подія НЕТ_ЧІТАТЕЛЕЙ у зайняте;
ЧІСЛО_ЧІТАТЕЛЕЙ + +;
/ / А ось тепер можна сміливо звільняти об'єкт ЕСТЬ_ПІСАТЕЛІ - під час
/ / Роботи читача досить мати скинуте подія НЕТ_ЧІТАТЕЛЕЙ Звільнити об'єкт ЕСТЬ_ПІСАТЕЛІ;
}
void ReleaseReader (NEWSWMRG * p) {
/ / Після зчитування даних зменшуємо лічильник і дозволяємо доступ письменникам
/ / При досягненні нульового значення лічильника.
- ЧІСЛО_ЧІТАТЕЛЕЙ;
if (ЧІСЛО_ЧІТАТЕЛЕЙ == 0) Встановити подія НЕТ_ЧІТАТЕЛЕЙ у вільний;
}
Природно, це ще не робоча схема, а тільки натяк на неї. Повністю обговоримо деякі особливості при розгляді функцій Win32 API. На перший погляд видно невеликий "прокол" - у функції ReleaseReader лічильник спочатку зменшується, а тільки потім відбувається перевірка його значення та встановлення події при необхідності. Проте можливий (хоча і дуже малоймовірний) випадок, коли потік буде перерваний для виконання операцій іншими потоками десь між зменшенням лічильника і установкою події. У цей час інші потоки можуть змінити значення лічильника і подія буде встановлено тоді, коли цього робити вже не слід.
Вийти з цієї ситуації можна різними шляхами - або додаванням ще одного об'єкта виключного володіння чи критичної секції для впорядкування операцій з лічильником, або іншими способами. Для розгляду з цими альтернативними способами слід розглянути синхронізацію з групою об'єктів.



[1] Можливі такі об'єкти, хоча це не типовий випадок, коли читачі не конфліктують з письменниками. Тоді в ситуаціях 2 і 3 слід дозволяти доступ.


[2] У Win32 API існують спеціальні механізми, що дозволяють відновлювати виконання тільки одного потоку при освобоженіі події. Проте, в порівнянні з іншими операційними системами, скажімо OS / 2, така поведінка події нетипово.



//ua-referat.com
скачати

© Усі права захищені
написати до нас