Семафор (програмування)

Семафор — це універсальний механізм для організації взаємодії процесів (в термінології операційних систем сімейства Windows — потоків).

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

Визначення семафору зроблено нідерландським вченим Едсгером Дейкстрою, деякий час використовувався термін Семафор Дейкстри.

Загальні дані ред.

Семафор — це об'єкт ядра ОС, який можна розглядати як лічильник, що містить ціле число в діапазоні від 0 до заданого при його створенні максимального значення[1]. При досягненні семафором значення 0 він переходить у несигнальний стан, при будь-яких інших значеннях лічильника — його стан сигнальний.

Традиційне позначення семафора: S. Операції, які можна виконати над семафором:

  1. Ініціалізація — встановлення початкового значення семафору.
  2. Операція P(S): Вона перевіряє стан семафору. Якщо семафор не рівний нулю, то виконується операція S:=S-1. Інакше, процес блокується, поки S=0.
  3. Операція V(S): Ця операція збільшує значення семафору на 1. Тобто виконується операція S:=S+1.

Типи семафорів ред.

В залежності від значень, які може приймати семафор він поділяється на:

  • Двійковий: здатний приймати значення 0 та 1.
  • Трійковий: здатний приймати значення 0, 1 та 2.
  • і т. д.

Задачі, що розв'язуються з допомогою семафорів ред.

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

Задача синхронізації ред.

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

Розв"язок задачі: Для розв'язання цієї задачі достатньо використати двійковий семафор(S=0,1). На початку обов'язково семафор S встановити в нуль: S:=0 !!!

 : Оскільки потоки виконуються паралельно, тобто водночас, то заздалегідь не відомо, який з потоків дійде до критичної ділянки коду першим. Існує два випадки:

  • Нехай першим дійшов потік А. Операція V(S) збільшить семафор на 1 і потік А продовжить виконуватися. Тоді, коли до критичної ділянки дійде потік В, він продовжить своє виконання. При цьому операція P(S) зменшить семафор на 1.
  • Нехай першим дійде потік В. Перевіривши семафор, він блокується, поки не буде повідомлення(сигналу) від потоку А. Тобто, поки семафор дорівнює нулю. Коли потік А відправить сигнал, тобто, збільшить семафор на 1, тоді продовжить виконуватися потік В, зменшивши при цьому семафор на 1.
Прикладом такої синхронізації може бути процес: Нехай потоку В для виконання потрібен ресурс, який формує потік А. Тому потоку В доводиться чекати, поки потік А не сформує цей ресурс.

Про завдання синхронізації потоків див. також конкуренція потоків;

Семафори у Windows ред.

Лічильник зменшується при кожному успішному завершенні функції очікування, яка використовує семафор і збільшується викликом функції Windows API ReleaseSemaphore. Тому семафор можна використовувати для обмеження доступу до ресурсу, який підтримує заздалегідь задану кількість під'єднань. На відміну від м'ютекса, стосовно семафора відсутнє поняття володіння.

Для створення семафора служить функція Windows API CreateSemaphore. Її опис мовою C[2]:

HANDLE WINAPI CreateSemaphore(
 __in_opt  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
 __in      LONG lInitialCount,
 __in      LONG lMaximumCount,
 __in_opt  LPCTSTR lpName
);

Опис мовою Delphi виглядає так[1]:

function CreateSemaphore(
 lpSemaphoreAttributes: PSecurityAttributes; // Атрибути доступу 
 lInitialCount,           // Початкове значення лічильника
 lMaximumCount: Longint;  // Максимальне значення лічильника
 lpName: PChar            // Ім'я об'єкта
): THandle;

Функція повертає дескриптор створеного семафора, або 0, якщо створити його не вдалося. Параметр lMaximumCount задає максимальне значення лічильника семафора, lInitialCount задає початкове значення лічильника й повинен бути в діапазоні від 0 до lMaximumCount. lpName містить ім'я семафора. Якщо в системі вже є семафор з таким ім'ям, то новий не створюється, а повертається дескриптор існуючого. При використанні семафора в середині одного процесу можна його створити без імені, передавши як lpName значення nil. Ім'я семафора не повинне збігатися з іменем уже існуючого об'єкта типу Event, Mutex, Waitable Timer, Job, або File Mapping.

Дескриптор раніше створеного семафора може бути отриманий функцією OpenSemaphore. Опис на C[3]:

HANDLE WINAPI OpenSemaphore(
 __in  DWORD dwDesiredAccess,
 __in  BOOL bInheritHandle,
 __in  LPCTSTR lpName
);

Опис функції мовою Delphi[1]:

function OpenSemaphore(
 dwDesiredAccess: DWORD;  // Задає права доступу до об'єкта
 bInheritHandle: BOOL;    // Задає, чи може семафор успадковуватися 
 lpName: PChar            // Ім'я об'єкта
): THandle;

Для збільшення лічильника семафора використовується функція ReleaseSemaphore. Її опис на C[4]:

BOOL WINAPI ReleaseSemaphore(
 __in       HANDLE hSemaphore,
 __in       LONG lReleaseCount,
 __out_opt  LPLONG lpPreviousCount
);

Опис мовою Delphi[1]:

function ReleaseSemaphore(
 hSemaphore: THandle;      // Дескриптор семафора
 lReleaseCount: Longint;   // Величина приросту семафора
 lpPreviousCount: Pointer  // Адреса змінної, яка прийме 
                           // попереднє значення
): BOOL;

Якщо значення лічильника після виконання функції перевищить заданий для нього максимум, то функція ReleaseSemaphore поверне false і значення семафора не зміниться. Параметром lpPreviousCount можна передати nil, якщо це значення програмі не потрібно. На відміну від м'ютекса, семафор не можна захопити у володіння, оскільки відсутнє саме поняття прав власності над ним. Крім цього, семафор може бути звільнений будь-яким потоком.

Приклад використання семафора ред.

Як приклад використання семафора розглянемо код, який дозволяє обмежити кількість екземплярів програми, які працюють одночасно (текст програми мовою Delphi[1]:).

var
 hSem   : Thandle;
 Cnt    : integer = 0;
 InstLim: integer = 3;
.................................................
const SemName = 'ISemaph';
.................................................
initialization
 hSem := CreateSemaphore(nil, InstLim, InstLim, SemName);
 // Якщо семафор не створено або він зайнятий
 if (hSem = 0) or (WaitForSingleObject(hSem, 100) <> WAIT_OBJECT_0) then
 begin
   ShowMessage('Допустима кількість програм вже працює!');
   Halt;
 end else Inc(Cnt);
finalization
 if hSem <> 0 then begin
   ReleaseSemaphore(hSem, Cnt, nil);
   CloseHandle(hSem);
 end;
end.

З цією метою в секції ініціалізації модуля головного вікна за допомогою функції CreateSemaphore робиться спроба створення семафора з назвою SemName (у змінній InstLim вказано максимально допустиму кількість запущених програм — 3). Якщо ця спроба невдала чи семафор не знаходиться у сигнальному стані (тобто допустима кількість програм вже запущена), виводиться повідомлення про неможливість запуску програми і викликається процедура Halt для завершення роботи (проте секція фіналізації виконається в будь-якому випадку). Якщо ж програму можна запускати, лічильник Cnt збільшується з 0 до 1.

Для звільнення використаних ресурсів при завершенні програми значення семафора збільшується на значення Cnt викликом функції ReleaseSemaphore. Якщо програму не буде запущено, Cnt=0 і виклик ReleaseSemaphore не збільшить значення семафора. У випадку, коли програма була запущена і завершує роботу, лічильник семафору зростає на 1, і якщо він був несигнальним, переводиться у сигнальний стан, дозволяючи таким чином запуск нового екземпляра програми. Після цього викликом CloseHandle звільняється дескриптор семафора.

Джерела ред.

Див. також ред.