Синхронізація процесів та потоків

Процесом (process) називається екземпляр програми, завантаженої на згадку. Цей екземпляр може створювати нитки (thread), які є послідовністю інструкцій виконання. Важливо розуміти, що виконуються не процеси, саме нитки.

Причому будь-який процес має хоча б одну нитку. Ця нитка називається головною (основною) ниткою програми.

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

Залежно від ситуації нитки можуть бути у трьох станах. По-перше, нитка може виконуватися, коли виділено процесорний час, тобто. вона може у стані активності. По-друге, може бути неактивною і очікувати виділення процесора, тобто. бути у стані готовності. І є ще третій, теж дуже важливий стан – стан блокування. Коли нитка заблокована, їй взагалі не вирізняється час. Зазвичай блокування ставиться на час очікування будь-якої події. У разі виникнення цієї події нитка автоматично переводиться зі стану блокування в стан готовності. Наприклад, якщо одна нитка виконує обчислення, а інша повинна чекати на результати, щоб зберегти їх на диск. Друга могла б використовувати цикл типу "while(!isCalcFinished) continue;", але легко переконатися на практиці, що під час виконання цього циклу процесор зайнятий на 100% (це називається активним очікуванням). Таких циклів слід по можливості уникати, в чому надає неоціненну допомогу механізм блокування. Друга нитка може заблокувати себе доти, доки перша невстановить подію, що сигналізує у тому, що читання закінчено.

Синхронізація ниток у Windows

У Windows реалізована багатозадачність, що витісняє - це означає, що в будь-який момент система може перервати виконання однієї нитки і передати управління іншою. Раніше, у Windows 3.1, використовувався спосіб організації, який називається кооперативною багатозадачністю: система чекала, поки нитка сама не передасть їй управління і саме тому у разі зависання однієї програми доводилося перезавантажувати комп'ютер.

приклад. Несинхронізована робота ниток: якщо тимчасово призупинити виконання нитки виведення на екран (пауза), фонова нитка заповнення масиву продовжуватиме працювати.

Саме тому необхідний механізм, що дозволяє потокам узгоджувати свою роботу із загальними ресурсами. Цей механізм отримав назву механізму синхронізації ниток (thread synchronization).

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

Об'єктів синхронізації є кілька, найважливіші їх - це взаємовиключення (mutex), критична секція (critical section), подія (event) і семафор (semaphore). Кожен із цих об'єктів реалізує свій спосіб синхронізації. Також як об'єкти синхронізації можуть використовуватися самі процеси та нитки (коли одна нитка чекає завершення іншої нитки абопроцесу); а також файли, комунікаційні пристрої, консольне введення та повідомлення про зміну.

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

Робота з об'єктами синхронізації

Щоб створити той чи інший об'єкт синхронізації, здійснюється виклик спеціальної функції WinAPI типу Create. (Напр. CreateMutex). Цей виклик повертає дескриптор об'єкта (HANDLE), який може використовуватись усіма нитками, що належать даному процесу. Є можливість отримати доступ до об'єкта синхронізації з іншого процесу - успадкувавши дескриптор цього об'єкта, або, що краще, скориставшись викликом функції відкриття об'єкта (Open. ). Після цього виклику процес отримає дескриптор, який можна використовувати для роботи з об'єктом. Об'єкту, якщо він не призначений для використання всередині одного процесу, обов'язково присвоюється ім'я. Імена всіх об'єктів мають бути різні (навіть якщо вони різного типу). Не можна, наприклад, створити подію і семафор з тим самим ім'ям.

За наявним дескриптором об'єкта можна визначити його поточний стан. Це робиться за допомогою т.зв. функцій, що очікують. Найчастіше використовується функція WaitForSingleObject. Ця функція приймає два параметри, перший з яких – дескриптор об'єкта, другий –час очікування мсек. Функція повертає WAIT_OBJECT_0, якщо об'єкт знаходиться в сигнальному стані, WAIT_TIMEOUT - якщо минув час очікування, і WAIT_ABANDONED, якщо об'єкт-взаємовиключення не було звільнено до того, як нитка, що володіє ним, завершилася. Якщо час очікування вказано рівним нулю, функція повертає результат негайно, інакше вона чекає протягом зазначеного проміжку часу. Якщо стан об'єкта стане сигнальним до закінчення цього часу, функція поверне WAIT_OBJECT_0, інакше функція поверне WAIT_TIMEOUT. Якщо в якості часу вказано символічну константу INFINITE, то функція чекатиме необмежено довго, доки стан об'єкта не стане сигнальним.

Дуже важливим є той факт, що звернення до очікуваної функції блокує поточну нитку, тобто. поки нитка перебуває у стані очікування, їй виділяється процесорного часу.

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

Об'єкт-критична секція допомагає програмісту виділити ділянку коду, де нитка отримує доступ до ресурсу, що розділяється, і запобігти одночасному використанню ресурсу. Перед використанням ресурсу нитка входить до критичної секції (викликає функцію EnterCriticalSection). Якщо після цього будь-яка інша нитка спробує увійти в ту саму критичну секцію, її виконання призупиниться, доки перша нитка не залишить секцію за допомогою виклику LeaveCriticalSection. Використовується лише для ниток одного процесу. Порядок входу до критичної секції не визначено.

Існує також функція TryEnterCriticalSection, яка перевіряє, чи зайнята критична секція в даний момент. З її допомогою нитка в процесі очікування на доступ до ресурсу може не блокуватися, а виконувати якісь корисні дії.

приклад. Синхронізація ниток за допомогою критичнихсекцій.

Взаємовиключення

Об'єкти-взаємовиключення (м'ютекси, mutex - від MUTual EXclusion) дозволяють координувати взаємне виключення доступу до ресурсу, що розділяється. Сигнальний стан об'єкта (тобто стан "встановлений") відповідає моменту часу, коли об'єкт не належить жодній нитці та його можна "захопити". І навпаки, стан "скинутий" (не сигнальний) відповідає моменту, коли якась нитка вже володіє цим об'єктом. Доступ до об'єкта дозволяється, коли нитка, яка володіє об'єктом, звільнить його.

Дві (або більше) нитки можуть створити м'ютекс з тим самим ім'ям, викликавши функцію CreateMutex. Перша нитка справді створює м'ютекс, а наступні – одержують дескриптор вже існуючого об'єкта. Це дає можливість кільком ниткам отримати дескриптор одного і того ж м'ютексу, звільняючи програміста від необхідності дбати про те, хто насправді створює м'ютекс. Якщо використовується такий підхід, бажано встановити прапор bInitialOwner у FALSE, інакше виникнуть певні труднощі щодо дійсного творця мьютексу.

Декілька ниток можуть отримати дескриптор одного і того ж м'ютексу, що уможливлює взаємодію між процесами. Можна використовувати такі механізми такого підходу:

  • Дочірній процес, створений за допомогою функції CreateProcess, може успадковувати дескриптор м'ютексу у випадку, якщо при створенні м'ютексу функцією CreateMutex був вказаний параметр lpMutexAttributes.
  • Нитка може отримати дублікат існуючого м'ютексу за допомогою функції DuplicateHandle.
  • Нитка може вказати ім'я існуючого м'ютексу під час виклику функцій OpenMutex або CreateMutex.

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

Для синхронізації ниток одного процесу ефективніше використання критичних секцій.

приклад. Синхронізація ниток за допомогою м'ютексів.

Об'єкти-події використовуються для попередження ниток про настання будь-якої події. Розрізняють два види подій – з ручним та автоматичним скиданням. Ручне скидання здійснюється функцією ResetEvent. Події з ручним скиданням використовуються для повідомлення одразу кількох ниток. При використанні події з автоскиданням повідомлення отримає і продовжить своє виконання тільки одна нитка, що очікує, інші чекатимуть далі.

Функція CreateEvent створює об'єкт-подію, SetEvent – ​​встановлює подію у сигнальний стан, ResetEvent – ​​скидає подію. Функція PulseEvent встановлює подію, а після відновлення тих, що очікують цю подію, ниток (всіх при ручному скиданні і тільки однієї при автоматичному), скидає її. Якщо ниток немає, PulseEvent просто скидає подію.

приклад. Синхронізація ниток за допомогою подій.

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

Функція CreateSemaphore створює об'єкт-семафор із зазначенням і максимально можливого початкового його значення, OpenSemaphore – повертає дескриптор існуючого семафору, захоплення семафора здійснюється за допомогою функцій, що очікують, при цьому значення семафору зменшується на одиницю, ReleaseSemaphore - звільнення семафора число.

приклад. Синхронізація ниток за допомогою семафорів.

Захищений доступ до змінних

Існує ряд функцій, що дозволяють працювати з глобальними змінними з усіх ниток, не переймаючись синхронізацією, т.к. ці функції самі за нею стежать - виконання атомарно. Це функції InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd та InterlockedCompareExchange. Наприклад, функція InterlockedIncrement збільшує атомарно значення 32-бітної змінної на одиницю, що зручно використовувати для різних лічильників.

Для отримання повної інформації про призначення, використання та синтаксис усіх функцій WIN32 API необхідно скористатися системою допомоги MS SDK, що входить до складу програмування Borland Delphi або CBuilder, а також MSDN, що поставляється у складі системи програмування Visual C.