Semaphore: що це, принцип роботи та застосування в синхронізації потоків

Автор: IT Sectr Опубліковано: 2026-03-19 Час читання: 8 хв

Semaphore — це примітив синхронізації, який керує доступом до спільного ресурсу через лічильник і чергу потоків, що очікують. За даними Wikipedia, 2024, семафор був запропонований Едсгером Дейкстрою в 1965 році для вирішення задач багатопотокової взаємодії. Інструмент дозволяє обмежити кількість потоків, які одночасно працюють з критичною секцією.

Головне

  • Semaphore — примітив синхронізації, що керує доступом через лічильник дозволів.
  • Двійковий семафор приймає значення 0 та 1, працюючи як прапорець блокування.
  • Лічильний семафор допускає одночасний доступ заданої кількості потоків.
  • На відміну від м'ютекса, семафор не прив'язаний до потоку-власника.
  • Deadlock — одна з головних небезпек при неправильному використанні семафорів.

Що таке Semaphore?

Semaphore — це примітив синхронізації, який використовує лічильник для керування доступом до спільного ресурсу. Концепція була запропонована Едсгером Дейкстрою в 1965 році і стала фундаментом для всіх сучасних механізмів синхронізації в операційних системах.

Визначення та призначення

Семафор являє собою цілочислову змінну з двома атомарними операціями: wait (acquire) та signal (release). Операція wait зменшує лічильник, а signal збільшує його. Коли лічильник досягає нуля, потік, який викликає wait, блокується до виконання signal іншим потоком.

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

Історія та теоретична основа

Концепція семафора виникла в контексті операційної системи THE, розробленої в Technische Hogeschool Eindhoven. Дейкстра формалізував семафор як математичну абстракцію, довівши його достатність для реалізації будь-яких примітивів синхронізації.

Як працює семафор?

Механізм семафора заснований на двох атомарних операціях і внутрішній черзі очікування. При виклику acquire потік перевіряє значення лічильника і або продовжує виконання, або блокується до звільнення ресурсу.

Лічильник та атомарні операції

При створенні семафора задається початкове значення лічильника дозволів. Кожен виклик acquire зменшує лічильник на 1. Якщо після цього лічильник стає від'ємним, потік блокується. Операція release збільшує лічильник і пробуджує один із потоків, що очікують.

kotlin
import java.util.concurrent.Semaphore

val semaphore = Semaphore(3)

fun accessResource() {
    semaphore.acquire()
    try {
        println("${Thread.currentThread().name} працює")
    } finally {
        semaphore.release()
    }
}

Черга очікування та планування

Коли потік викликає acquire при нульовому лічильнику, ОС поміщає його в чергу FIFO семафора. Потік переходить у стан BLOCKED, не споживаючи процесорний час. Після виклику release перший потік у черзі переводиться в стан RUNNABLE та отримує доступ до ресурсу.

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

У теорії синхронізації виділяють два основних типи семафорів: двійковий (binary) та лічильний (counting). Вибір типу залежить від конкретної задачі керування доступом до ресурсів.

Двійковий семафор (Binary Semaphore)

Двійковий семафор приймає тільки значення 0 та 1. За поведінкою він нагадує м'ютекс, але без вимоги володіння — будь-який потік може виконати release. Такі семафори зручні для реалізації прапорців готовності та подій між потоками.

kotlin
val ready = Semaphore(0)

fun producer() {
    Thread.sleep(1000)
    ready.release()
}

fun consumer() {
    ready.acquire()
    println("Дані готові")
}

Лічильний семафор (Counting Semaphore)

Лічильний семафор може приймати будь-яке невід'ємне значення. Він використовується для керування пулом однотипних ресурсів, де доступно кілька екземплярів. Наприклад, пул із 5 мережевих з'єднань: кожен acquire займає одне з'єднання, release повертає його в пул.

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

ПараметрДвійковий семафорЛічильний семафор
Діапазон0 або 1від 0 до N
Потоків одночасно1до N
Застосуваннясигналізація, прапорціпули ресурсів, rate limiting

Semaphore vs М'ютекс

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

Принцип володіння

Ключова відмінність — концепція володіння. М'ютекс завжди знає, який потік його захопив, і тільки цей потік може його звільнити. Семафор не має власника: будь-який потік може викликати release, навіть не викликаючи acquire. Це робить м'ютекс безпечнішим для захисту даних, а семафор — гнучкішим для координації.

Продуктивність та сценарії використання

На практиці м'ютекс швидший для простої взаємної блокіровки завдяки оптимізаціям під типовий сценарій. Семафор вимагає додаткових накладних витрат на підтримку лічильника. Однак для обмеження паралелізму або реалізації шаблону «виробник-споживач» семафор незамінний.

ХарактеристикаSemaphoreМ'ютекс
Володіннянемає власникає власник
Звільненнябудь-який потіктільки потік-власник
Лічильниквід 0 до Nдвійковий
Use caseобмеження паралелізму та сигналізаціязахист критичної секції
Рекурсіянітак (reentrant)

Застосування Semaphore в мобільній розробці

У розробці мобільних застосунків Semaphore використовується для керування доступом до обмежених ресурсів: мережевих з'єднань, файлів, баз даних та апаратних компонентів. Сучасні платформи надають зручні вбудовані реалізації.

Пул з'єднань із сервером

Один із типових кейсів — пул HTTP-з'єднань. Застосунок може одночасно надсилати не більше 4 запитів до сервера, оскільки API провайдера обмежує паралелізм. Семафор із початковим значенням 4 гарантує, що при будь-якому навантаженні кількість одночасних запитів не перевищить ліміт, а решта потоків чекатимуть у черзі.

Без семафора при різкому збільшенні користувацької активності серверна інфраструктура може отримати раптове перевантаження, що призводить до таймаутів і помилок 429 Too Many Requests. Semaphore працює як запобіжник, пропускаючи строго задану кількість одночасних викликів незалежно від кількості активних потоків.

Semaphore в Kotlin для Android

Android надає клас Semaphore з пакета java.util.concurrent. Розглянемо приклад обмеження одночасних мережевих запитів до двох потоків для запобігання перевантаження сервера.

kotlin
class ApiClient {
    private val throttle = Semaphore(2)

    suspend fun fetch(url: String): Result {
        throttle.acquire()
        return try {
            httpGet(url)
        } finally {
            throttle.release()
        }
    }
}

DispatchSemaphore в Swift для iOS

В iOS DispatchSemaphore із GCD вирішує те саме завдання. Розробники використовують його для синхронізації доступу до ресурсів в асинхронному коді без блокування головного потоку.

swift
let semaphore = DispatchSemaphore(value: 3)

func processBatch(_ items: [UIImage]) {
    for img in items {
        semaphore.wait()
        DispatchQueue.global().async {
            applyFilter(to: img)
            semaphore.signal()
        }
    }
}

Типові помилки при роботі з семафорами

Найчастіша помилка — забутий release при винятку. Якщо потік завершується з помилкою до виклику release, семафор назавжди залишається заблокованим для інших потоків. Використовуйте try/finally або defer для гарантованого звільнення. Друга проблема — deadlock при захопленні кількох семафорів у різному порядку різними потоками.

Паттерни використання семафорів

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

Існує кілька перевірених паттернів застосування семафорів у реальних проєктах. Їх знання допомагає уникнути типових помилок і будувати надійні багатопотокові системи.

Rate Limiter (обмеження швидкості)

Семафор із початковим значенням N і періодичним release через таймер реалізує обмеження швидкості запитів до API. Наприклад, сервіс дозволяє 10 запитів на секунду: семафор стартує з 10, кожен запит зменшує лічильник, а окремий TimerTask раз на секунду повертає лічильник до початкового значення. Це захищає і застосунок, і сервер від перевантаження.

Producer-Consumer через семафори

У класичній задачі виробник-споживач два семафори керують буфером: empty (дозволи на запис) та full (дозволи на читання). Producer викликає acquire у empty та release у full, Consumer — навпаки. Така схема гарантує, що Consumer ніколи не прочитає порожній буфер, а Producer не переповнить його.

Ця ж схема лежить в основі bounded buffer в операційних системах — кільцевого буфера з фіксованим розміром. У мобільних застосунках паттерн застосовується для обробки черг зображень, відеофайлів та аналітичних подій.

Throttling мережевих запитів

Семафори успішно використовуються для троттлінгу мережевих викликів у фонових сервісах. Наприклад, застосунок аналітики надсилає на сервер пакети подій. Без обмеження concurrent-потоків при пікових навантаженнях (запуск застосунку, синхронізація після офлайну) кількість одночасних запитів може перевищити ліміти сервера. Семафор із початковим значенням 3 гарантує плавну відправку та запобігає блокуванню на стороні сервера.

Часті запитання

У чому різниця між Semaphore і звичайним лічильником?

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

Чи може семафор стати причиною deadlock?

Так, deadlock можливий при захопленні кількох семафорів у різному порядку різними потоками. Наприклад, потік A захоплює S1, потім S2, а потік B — S2, потім S1. Фіксуйте єдиний порядок захоплення для всіх семафорів у проєкті.

Що відбувається при acquire з нульовим лічильником?

Потік блокується і переходить у стан очікування. Він не споживає процесорний час доти, доки інший потік не викличе release. У Java це стан BLOCKED, у Swift потік призупиняється GCD.

Чим Binary Semaphore принципово відрізняється від Mutex?

Основна відмінність — володіння. Mutex можна звільнити тільки з потоку-власника. Binary Semaphore звільняється будь-яким потоком, що зручно для сигналізації між потоками, але менш безпечно для захисту цілісності даних.

Яке початкове значення вибрати для лічильника?

Початкове значення залежить від сценарію. Для захисту одного ресурсу — 1. Для пулу з N з'єднань — N. Для сигналізації між потоками використовуйте 0, щоб потік-споживач чекав сигналу від виробника.

Підсумки

  • Semaphore — примітив синхронізації на основі лічильника, запропонований Дейкстрою в 1965 році.
  • Двійковий семафор приймає значення 0 та 1, лічильний — будь-яке невід'ємне значення.
  • Операції acquire та release є атомарними та потокобезпечними.
  • На відміну від м'ютекса, семафор не прив'язаний до потоку-власника.
  • Лічильні семафори застосовуються для керування пулами ресурсів та обмеження паралелізму.
  • Забутий release — найпоширеніша помилка, що веде до зависання потоків.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також