# Tokio: искусство синхронизации. Как победить великана с помощью Semaphore, Barrier и Mutex

> История экспедиции экипажа Vectoria — наглядное объяснение примитивов синхронизации в Tokio: Semaphore для ограничения мест, Barrier для совместного теплового удара, и Mutex для безопасного изменения здоровья врага.

- **URL:** https://eagle2000.space/rust/tokio-sync-primitives/
- **Date:** 2026-08-14

- **Tags:** rust, dev, tokio

---## Вступление

Иногда в мире асинхронного программирования задачи напоминают неорганизованную толпу. Каждая из них рвется вперед, пытается захватить общие ресурсы, прочитать или изменить одни и те же переменные. Без четкого плана такой хаос приводит к гонкам данных, взаимным блокировкам (deadlocks) и пустой трате ресурсов.

Чтобы понять, как обуздать этот хаос, мы отправимся вместе с экипажем исследовательского корабля _«Vectoria»_ в экспедицию на раскаленную планету Терра-Вулкания. Капитан Нова, инженер Спаркс и робот RUST-Y окажутся свидетелями невероятной природной схватки, которая наглядно объясняет, зачем нужны **примитивы синхронизации** в Tokio.

{{< alert "comment" >}}
В конце статьи вас ждет интерактивный тест по примитивам синхронизации Tokio, который поможет закрепить пройденный материал. Читайте внимательно!
{{< /alert >}}

---

## Пролог. Экспедиция на Вулканию

Пылевые вихри багрового цвета бились о лобовое стекло посадочного модуля. *Vectoria* с глухим металлическим скрежетом коснулась базальтовой плиты.

— «Системы стабилизированы. Температура за бортом — триста градусов по Цельсию», — доложил RUST-Y, сверкнув зелеными фотодиодами. — «Но датчики фиксируют аномальную сейсмическую активность в трехстах метрах к югу».

Капитан Нова защелкнул шлем скафандра:
— «Спаркс, Расти — за мной. Посмотрим, что там шевелится на этих лавовых озерах».

Выбравшись наружу, экспедиция подошла к краю глубокого каньона. На дне каньона на базальтовом выступе громоздилось огромное дикое гнездо плазменных пчел. Вдруг небо потемнело. Огромная тень спустилась со скал. Это был **Кремниевый Шершень** — летающий хищник размером с орла, покрытый прочной каменной броней и вооруженный бритвенно-острыми жвалами.

Шершень бросился на гнездо, намереваясь разорить его.

---

## Битва в долине

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

Сначала битва выглядела трагично. Пчелы взлетали поодиночке и пытались жалить Шершня, нагревая свои тела до высоких температур. Но одиночный нагрев пчелы был ничтожно мал для каменного панциря хищника — тепло мгновенно рассеивалось, а Шершень с легкостью сбивал защитников своими жвалами. 

— «Они погибают ни за что!» — взволнованно произнес Расти. — «Их атаки хаотичны!»

Но внезапно поведение роя изменилось. Пчелы перестали атаковать вразнобой. Они выстроились на стенах гнезда. Когда Шершень подлетел ближе, первая группа пчел быстро и упорядоченно запрыгнула на его грудной отдел. Они занимали позиции строго по очереди, следя за тем, чтобы не сталкиваться и не мешать друг другу. Свободных мест на теле Шершня было мало, и пчелы плотно покрыли его стыки брони.

Закрепившись на враге, они не стали сразу выделять тепло. Пчелы замерли. Они ждали, пока на панцире соберется критическая масса участников.

Как только пятая пчела зацепилась за панцирь, завершив живую оболочку, весь тепловой шар **одновременно** завибрировал крыльями. Рой вспыхнул ослепительным плазменным светом. Температура внутри шара мгновенно подскочила до пятисот градусов. Кремниевый Шершень задергался, его внутренние кремниевые узлы перегрелись, панцирь лопнул, и огромный хищник бездыханно рухнул на лавовые камни.

---

## От биологии к коду

Экспедиция ошеломленно молчала. Наконец Расти нарушил тишину:
— «Это было потрясающе! Они действовали как единая система. Но как они координировали свои действия? Сначала ограничили число сидящих на шершне, потом ждали сбора группы, а затем одновременно выделили тепло...»

Капитан Нова улыбнулся и переключил визор в режим терминала:
— «Это идеальное биологическое воплощение примитивов синхронизации, Расти. Если бы мы писали программу симуляции этой битвы в нашей асинхронной системе Tokio на Rust, мы бы использовали те же самые концепции. Давай разберем, как бы выглядел их алгоритм в коде!»

---

## Часть 1. Хаос одиночек (Без координации)

Если бы мы просто запустили сотни задач-пчел в Tokio без всякой синхронизации, планировщик выполнял бы их хаотично. Каждая пчела нагревается в случайное время, тепло рассеивается, а шершень побеждает их поодиночке.

Вот как выглядит этот хаотичный алгоритм в коде:


### 💻 Хаотичная атака без координации


```rust
use std::time::Duration;
use tokio::time::sleep;
async fn attack_individually(id: u32) {
    println!("[Пчела {}] Лечу атаковать Шершня в одиночку!", id);
    sleep(Duration::from_millis(50)).await;
    // Без сбора группы тепло одной пчелы мгновенно рассеивается
    println!("[Пчела {}] Меня смахнули! Мое тепло рассеялось, я погибла.", id);
}

#[tokio::main]
async fn main() {
    let mut tasks = vec![];
    for id in 1..=5 {
        tasks.push(tokio::spawn(attack_individually(id)));
    }
    for task in tasks {
        let _ = task.await;
    }
    println!("[ИТОГ] Шершень не заметил атаки. Все пчелы погибли.");
}
```

**Хаотичная атака: без координации**
- Каждая задача запускается независимо и пытается нагреться сразу же.
- Без синхронизации пчелы действуют вразнобой, тепло рассеивается, и шершень легко побеждает их поодиночке.


Без синхронизации наши асинхронные задачи тратят ресурсы впустую, не нанося вреда Шершню.

---

## Часть 2. Семафор — занимаем позиции на броне

— «Чтобы пчелы не мешали друг другу и занимали ограниченные места на панцире Шершня, нам нужен регулятор доступа», — продолжил капитан.

— «В Tokio для этого используется **`Semaphore`** (Семафор). Представь, что на Шершне есть всего `N` свободных мест. Семафор выдает виртуальные разрешения — `permits`. Задача вызывает `acquire().await`. Если места есть, задача получает разрешение и крепится к цели. Если мест нет — задача асинхронно ждет своей очереди».


### 💻 Семафор: ограничение свободных мест


```rust
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Semaphore;
use tokio::time::sleep;
async fn try_cling(id: u32, semaphore: Arc<Semaphore>) {
    println!("[Пчела {}] Ищет свободное место на теле Шершня...", id);
    
    // Получаем разрешение (permit) на посадку. Если мест нет — ждем.
    let _permit = semaphore.acquire().await.unwrap();
    
    println!("[Пчела {}] Зацепилась за панцирь! (Место занято)", id);
    sleep(Duration::from_millis(200)).await; // Удерживаем место на Шершне
    
    println!("[Пчела {}] Отпускает Шершня. (Место освободилось)", id);
    // _permit автоматически возвращается в семафор при выходе из области видимости
}

#[tokio::main]
async fn main() {
    // На теле Шершня одновременно могут закрепиться только 2 пчелы
    let hornet_surface = Arc::new(Semaphore::new(2));
    let mut tasks = vec![];

    for id in 1..=4 {
        let sem = hornet_surface.clone();
        tasks.push(tokio::spawn(try_cling(id, sem)));
    }

    for task in tasks {
        let _ = task.await;
    }
}
```

**Семафор: ограничение доступа к ресурсу**
- `tokio::sync::Semaphore` регулирует доступ к ограниченному ресурсу (2 места на теле Шершня).
- Вызов `acquire().await` блокирует задачу асинхронно, если все разрешения (permits) заняты.
- Разрешение автоматически освобождается при уничтожении `SemaphorePermit`.


{{< alert "lightbulb" >}}
**Принцип RAII в действии:** Обратите внимание, что мы не возвращаем разрешение в семафор вручную. Объект `SemaphorePermit` удерживается в переменной `_permit` и автоматически возвращает место в пул семафора, как только эта переменная выходит из области видимости в конце асинхронной функции.
{{< /alert >}}

---

## Часть 3. Барьер — единый тепловой удар

— «Но семафор лишь помог занять места. Как заставить пчел вспыхнуть строго в одно мгновение?» — спросил Спаркс.

— «Для этого служит **`Barrier`** (Барьер)», — ответил Нова. — «Мы инициализируем барьер на `M` участников. Каждая пчела, закрепившись на броне шершня, вызывает `barrier.wait().await` и приостанавливает выполнение. Как только последний, `M`-й участник вызывает этот метод — барьер открывается, и все задачи просыпаются одновременно, нанося разрушительный тепловой удар».


### 💻 Барьер: групповой залп задач


```rust
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Barrier;
use tokio::time::sleep;
async fn attack_with_barrier(id: u32, barrier: Arc<Barrier>) {
    println!("[Пчела {}] Зацепилась и ждет остальных...", id);
    
    // Ждем, пока все участники группы (3 пчелы) достигнут этой точки
    barrier.wait().await;
    
    // Все 3 пчелы на месте — запускаем нагрев одновременно!
    println!("[Пчела {}] Вспышка! Запускаем термо-нагрев!", id);
    sleep(Duration::from_millis(50)).await;
}

#[tokio::main]
async fn main() {
    // Барьер ожидает ровно 3 участников для проведения атаки
    let attack_barrier = Arc::new(Barrier::new(3));
    let mut tasks = vec![];

    for id in 1..=3 {
        let barrier = attack_barrier.clone();
        tasks.push(tokio::spawn(attack_with_barrier(id, barrier)));
    }

    for task in tasks {
        let _ = task.await;
    }
    println!("[ИТОГ] Шершень побежден тепловым шаром!");
}
```

**Барьер: одновременный запуск группы задач**
- `tokio::sync::Barrier` приостанавливает задачи до тех пор, пока нужное число участников не вызовет `wait()`.
- Как только пришел последний участник, все задачи возобновляют работу одновременно.
- Это гарантирует синхронный разогрев всей группы пчел в один момент.


---

## Часть 4. Мьютекс — здоровье врага под защитой

— «Но постойте!» — Расти вывел на экран схему. — «Показатель здоровья Шершня (HP) — это общая переменная в памяти. Если несколько пчел начнут одновременно изменять ее из разных задач, возникнет гонка данных!»

Нова кивнул:
— «Верно. Для защиты общего состояния мы используем **`Mutex`** (Мьютекс). Он гарантирует, что только одна задача может работать с данными в конкретный момент времени. Но в асинхронном Rust с мьютексами связана критическая ловушка».

Давайте посмотрим на эту ошибку и разберемся, почему стандартный `std::sync::Mutex` может сломать компиляцию в Tokio:


### 💻 Мьютекс: изменение общего состояния


#### Шаг 1

```rust
// ?compile_fail
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::sleep;
async fn deal_damage_bad(id: u32, hornet_hp: Arc<Mutex<i32>>) {
    // Получаем блокировку стандартного std::sync::Mutex
    let mut hp = hornet_hp.lock().unwrap();
    *hp -= 10;
    println!("[Пчела {}] Разогрела панцирь. Текущее HP Шершня: {}", id, *hp);
    
    // Пытаемся уснуть, удерживая std::sync::MutexGuard.
    // ОШИБКА: MutexGuard не реализует Send, и Tokio не сможет перенести
    // эту задачу на другой поток после .await!
    sleep(Duration::from_millis(10)).await; 
}

#[tokio::main]
async fn main() {
    let hornet_hp = Arc::new(Mutex::new(100));
    // Этот код не скомпилируется!
    tokio::spawn(deal_damage_bad(1, hornet_hp.clone())).await.unwrap();
}
```

**Ошибка: удержание std::sync::MutexGuard через .await**
- Стандартный `std::sync::MutexGuard` не является `Send`, так как привязан к конкретному потоку ОС.
- Если удерживать его во время вызова `.await`, компилятор выдаст ошибку, так как Tokio не сможет безопасно переносить задачу между потоками во время ожидания.


#### Шаг 2

```rust
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::sleep;
async fn deal_damage_good(id: u32, hornet_hp: Arc<Mutex<i32>>) {
    // Асинхронный Tokio MutexGuard безопасен для удерживания через .await
    let mut hp = hornet_hp.lock().await;
    *hp -= 10;
    println!("[Пчела {}] Разогрела панцирь. Текущее HP Шершня: {}", id, *hp);
    
    // Спим, удерживая блокировку. Это работает, но блокирует остальных!
    sleep(Duration::from_millis(10)).await;
}

#[tokio::main]
async fn main() {
    let hornet_hp = Arc::new(Mutex::new(100));
    let mut tasks = vec![];
    for id in 1..=3 {
        let hp = hornet_hp.clone();
        tasks.push(tokio::spawn(deal_damage_good(id, hp)));
    }
    for task in tasks {
        let _ = task.await;
    }
    println!("[ИТОГ] Здоровье Шершня успешно уменьшено до 70.");
}
```

**Решение: Использование асинхронного tokio::sync::Mutex**
- `tokio::sync::Mutex` возвращает guard, который реализует `Send` и может безопасно удерживаться через точки `.await`.
- Альтернатива для производительности — использовать `std::sync::Mutex`, но ограничить область его видимости фигурными скобками `{}` так, чтобы блокировка снималась ДО вызова `.await`.


### Почему ломается компиляция при удерживании `std::sync::MutexGuard` через `.await`?

В Tokio работает многопоточный планировщик. Он распределяет задачи по пулу потоков ОС. Когда задача доходит до точки ожидания (`.await`), она приостанавливается, уступая место другим. Когда задача просыпается, планировщик может продолжить ее выполнение на **другом** потоке ОС.

Чтобы это было возможно, все данные, удерживаемые внутри функции `async` между точками `.await`, должны реализовывать трейт `Send` (безопасный перенос между потоками).
Однако стандартный `std::sync::MutexGuard` привязан к потоку ОС, на котором был взят замок, и **не является `Send`**. Компилятор Rust видит, что блокировка удерживается во время `.await` сна, и запрещает компиляцию.

### Как исправить эту проблему?

Есть два основных пути:
1. **Использовать асинхронный `tokio::sync::Mutex`**. Его guard реализует `Send` и может безопасно пережить `.await`. Однако помните: асинхронный мьютекс медленнее. Используйте его только тогда, когда вам действительно необходимо удерживать замок во время асинхронных операций ожидания.
2. **Ограничить область видимости блокировки**. Если внутри критической секции нет вызовов `.await`, используйте обычный `std::sync::Mutex`, но оберните работу с ним в отдельный блок `{}`. Как только блок закроется, guard будет уничтожен, и вы сможете спокойно вызывать `.await`.

---

## Жизненный цикл скоординированной атаки

Давайте визуализируем, как наши Плазменные пчелы координируют свои действия с точки зрения примитивов синхронизации Tokio:

<div style="background-color:white; padding: 20px">
{{< mermaid >}}
graph TD
    A[Пчела готова к атаке] --> B{Семафор: Есть места?}
    B -- Нет --> C[Ждет свободного места]
    C --> B
    B -- Да --> D[Занимает место / Берет Permit]
    D --> E{Барьер: Собралась группа?}
    E -- Нет --> F[Ждет остальных участников]
    F --> E
    E -- Да --> G[Вспышка / Проход Барьера]
    G --> H[Мьютекс: Захват здоровья Шершня]
    H --> I[Нанесение урона HP]
    I --> J[Освобождение Мьютекса]
    J --> K[Спрыгивает с Шершня / drop Permit]
    K --> L[Победа над Шершнем]

    classDef startEnd fill:#8B5CF6,stroke:#6D28D9,stroke-width:2px,color:#fff;
    classDef action fill:#F8FAFC,stroke:#E2E8F0,stroke-width:2px,color:#334155;
    classDef decision fill:#F97316,stroke:#EA580C,stroke-width:2px,color:#fff;
    classDef sync fill:#06B6D4,stroke:#0891B2,stroke-width:2px,color:#fff;
    classDef lock fill:#EC4899,stroke:#DB2777,stroke-width:2px,color:#fff;
    classDef success fill:#10B981,stroke:#059669,stroke-width:2px,color:#fff;

    class A startEnd;
    class B,E decision;
    class C,F sync;
    class D,G,I,K action;
    class H,J lock;
    class L success;
{{< /mermaid >}}
</div>

---

## Приложение. Шпаргалка капитана по примитивам синхронизации

Держите этот краткий перечень примитивов синхронизации в своем бортовом журнале, чтобы всегда выбирать правильный инструмент:

| Примитив | Для чего нужен | Пример из жизни роя |
|---|---|---|
| **`Semaphore`** | Ограничивает количество параллельно выполняемых задач (доступ к ресурсу) | Количество пчел, которые могут одновременно залезть на тело Шершня. |
| **`Barrier`** | Заставляет группу задач ждать друг друга, чтобы продолжить работу одновременно | Синхронизация теплового удара пчел строго при накоплении группы. |
| **`Mutex`** | Предоставляет эксклюзивный доступ к изменению разделяемых данных (взаимное исключение) | Изменение уровня здоровья Шершня несколькими пчелами без гонок данных. |
| **`RwLock`** | Позволяет множеству читателей читать данные одновременно, но только одному писателю записывать | Совместное сканирование уязвимостей Шершня всеми пчелами, но обновление карты гнезда только маткой. |
| **`Notify`** | Простой механизм оповещения одной задачи другой задачей (сигнал «проснись») | Сигнал тревоги от пчелы-разведчика всему рою при появлении угрозы. |

Экспедиция на Терра-Вулканию успешно завершилась. Экипаж *Vectoria* получил ценный урок: даже самые маленькие и слабые асинхронные задачи способны победить любые вычислительные трудности, если они действуют слаженно и используют правильные инструменты синхронизации.

---

## Финальный квиз 🚀

Проверьте, насколько хорошо вы усвоили примитивы синхронизации Tokio, чтобы уверенно применять их в своих проектах:


### 📝 Tokio: примитивы синхронизации


**1. Какую основную задачу решает tokio::sync::Semaphore?**

- [ ] Позволяет разделять данные между потоками с эксклюзивным доступом к записи
- [ ] Ограничивает количество задач, которые могут одновременно выполнять определенный код или использовать ресурс
- [ ] Пересылает сообщения от одного отправителя к множеству получателей
- [ ] Останавливает выполнение всех потоков до завершения одной задачи

**2. Как ведет себя tokio::sync::Barrier?**

- [ ] Запрещает запись в переменную, пока идет ее чтение
- [ ] Ожидает наступления определенного времени суток
- [ ] Блокирует переданные в него задачи до тех пор, пока их общее количество на барьере не достигнет заданного N, после чего все они возобновляются одновременно
- [ ] Передает управление планировщику Tokio для переключения на другие задачи

**3. Почему компилятор выдает ошибку, если удерживать std::sync::MutexGuard через вызов .await?**

- [ ] Стандартный MutexGuard автоматически закрывает поток ОС
- [ ] std::sync::MutexGuard не реализует трейт Send, а рантайм Tokio может переносить асинхронные задачи между потоками операционной системы во время ожидания .await
- [ ] Мьютекс из стандартной библиотеки слишком медленный для асинхронного кода
- [ ] В Rust запрещено использовать блокировки внутри функций async

**4. Как освобождается разрешение (permit) семафора в Tokio в идиоматичном коде?**

- [ ] Только путем ручного вызова метода semaphore.release()
- [ ] Путем вызова функции drop(permit) или когда SemaphorePermit автоматически выходит из области видимости
- [ ] При перезапуске планировщика Tokio
- [ ] Разрешения семафора одноразовые и не освобождаются

**5. Как исправить ошибку компиляции 'MutexGuard across .await' без использования относительно медленного асинхронного tokio::sync::Mutex?**

- [ ] Использовать unsafe блоки для обхода ограничений компилятора
- [ ] Переписать весь код на синхронные потоки std::thread
- [ ] Ограничить область видимости блокировки фигурными скобками {} или вызвать drop(guard) до вызова .await
- [ ] Заменить мьютекс на std::sync::Arc

