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 в 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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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