Что такое…

Что такое семафор в программировании — и почему пачки запросов медленнее

Иллюстрация: парковка на пять мест со шлагбаумом и очередь машин, которые въезжают по одной, как только кто-то уезжает

Тебе надо отправить 100 запросов к API. Все сразу нельзя — сервер ответит ошибкой 429. Поэтому ты делишь их на пачки по пять: отправил пять, дождался, отправил следующие пять.

Работает. Но медленно. Почему? В каждой пятёрке есть один медленный запрос на 5 секунд. Остальные четыре закончились за секунду — и стоят, ждут его. Двадцать пачек по 5 секунд — 100 секунд.

А можно по-другому: держать в работе ровно пять запросов в любой момент. Один закончился — сразу стартует следующий. Те же 100 запросов, тот же лимит — около 40 секунд. Штука, которая так делает, называется семафор.

Что такое семафор

Семафор — это счётчик свободных мест. Он пускает к ресурсу не больше N задач одновременно. Остальные ждут в очереди.

Представь парковку на пять мест со шлагбаумом. Пока есть место — шлагбаум открыт. Все пять заняты — следующая машина ждёт. Одна уехала — одна въехала. Парковка никогда не пустует, если есть очередь. И никогда не переполнена.

Придумал семафоры Эдсгер Дейкстра в 1960-х. Тогда это было про процессы в операционной системе. Сейчас — про запросы, файлы и подключения к базе.

У семафора есть родственник — мьютекс. Мьютекс пускает строго одного. По сути это семафор на одно место.

Как он работает

У семафора две операции:

  1. Взять место. Если счётчик больше нуля — уменьши его и проходи. Если ноль — жди.
  2. Вернуть место. Увеличь счётчик и разбуди первого в очереди.

Вот и всё. В JavaScript семафор для запросов пишется меньше чем в двадцать строк:

function createLimit(max) {
  let active = 0
  const queue = []
  const next = () => {
    if (active >= max || queue.length === 0) return
    active++
    const { task, resolve, reject } = queue.shift()
    task().then(resolve, reject).finally(() => {
      active--
      next()
    })
  }
  return (task) => new Promise((resolve, reject) => {
    queue.push({ task, resolve, reject })
    next()
  })
}

А пользоваться им так:

const limit = createLimit(5)
const results = await Promise.all(
  urls.map((url) => limit(() => fetch(url)))
)

Обрати внимание на () => fetch(url). Мы передаём не запрос, а функцию, которая его запустит. Если написать просто limit(fetch(url)), все 100 запросов улетят сразу, и семафор будет охранять пустоту. Это самая частая ошибка.

Не хочешь писать сам — есть готовая библиотека p-limit. Работает так же.

Почему это важно тебе

Почти любой внешний сервис ограничивает частоту запросов. Подробно — в статье что такое rate limit. Отправишь всё через Promise.all — получишь 429. Будешь слать по одному — прождёшь вечность. Семафор — золотая середина.

Особенно это видно с ИИ. Ты прогоняешь через модель 300 описаний товаров. У API лимит на число одновременных запросов. Семафор на 5–10 мест — и скрипт идёт на полной скорости, не упираясь в ошибки.

И это не только про сеть. Ты обрабатываешь тысячу картинок — каждая съедает память. Запустишь все разом — упадёшь с нехваткой памяти. Семафор на четыре — и процессор занят, а память в порядке.

Если для тебя await и промисы пока туманны, начни со статьи что такое асинхронность.

Где ты уже встречал семафор

Ты им пользуешься каждый день, просто не знаешь.

  • Браузер. По старому протоколу HTTP/1.1 Chrome держит не больше шести соединений с одним сайтом. Седьмая картинка ждёт, пока освободится место. Это семафор на шесть.
  • База данных. У PostgreSQL по умолчанию 100 подключений. Сто первое получит ошибку sorry, too many clients already. Пул подключений — тот же семафор: он раздаёт соединения по очереди.
  • Python и Go. В Python есть готовый asyncio.Semaphore. В Go роль семафора играет буферизованный канал.

Сколько мест ставить в семафоре?

Начни с того, что пишет документация API. Если там тишина — 5. Ловишь 429 — уменьши. Всё быстро и без ошибок — попробуй 10.

Семафор защищает от гонок?

Только семафор на одно место — то есть мьютекс. Если мест пять, пять задач могут одновременно менять одни и те же данные. Тогда гонка никуда не денется.

Что будет, если задача упадёт с ошибкой?

В коде выше место вернётся всё равно — за это отвечает finally. Без него одна ошибка навсегда «съедала» бы место, и рано или поздно очередь встала бы намертво.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →