# Делать много вещей разом

Экспресс-курс о параллелизме и async. 19 понятий — Concurrency и Parallelism, Process и Thread, Context Switch, Blocking и Async/Await, Event Loop, Race Condition, Lock, Deadlock, Message Passing, Immutability, Atomic Operation и другие — у каждого английское название, образ, схема и карточка с примером и ценой. В конце — карта связей и шпаргалка на одной странице. Главная мысль: делать много вещей разом — это как софт остаётся быстрым, а все баги идут от двух вещей, что трогают одни данные в один момент.

- **Author:** Фёдор Чищин
- **Level:** intermediate
- **Reading time:** ~30 минут чтения
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/ru/learn/concurrency-express

---

## 1. Concurrency против parallelism

Два слова используют так, будто они значат одно, а это не так. Одно про жонглирование; другое про много рук. Разложить их по местам — фундамент для всего остального.

### Карта: 19 понятий в шести группах

> Карта метро: сначала видишь линии целиком, потом едешь по станциям.

Сначала **основы** — что такое concurrency, parallelism и ядро процессора. Дальше пять групп, по одной на главу: **потоки и процессы**, **ожидание** (blocking, async, event loop), **общее состояние** и гонки, **блокировки** и их опасности, и **более безопасные паттерны**, что убирают гонку в корне.

_Diagram: Шесть групп понятий — по ним идут главы 1–6. Глава 7 собирает всё вместе._

### Как читать каждое понятие

> Паспорт: одинаковые поля у всех, поэтому их легко сравнивать.

У каждого понятия дальше одно и то же: **английское название** (запомни его — это термин, по нему ищут в документации), образ из жизни, **схема** и **карточка** с полями: _проблема_, _идея_, _пример_, _цена_ и _связи_ с другими понятиями. В конце — карта связей и шпаргалка.

### Concurrency — конкурентность, жонглирование задачами

> Один повар ведёт четыре блюда — режет, потом мешает, потом проверяет духовку, быстро переключаясь между ними.

**Concurrency** (конкурентность) — это управление многими задачами через переключение между ними: один работник продвигает несколько дел, но никогда не делает два в один и тот же миг. Один повар, жонглирующий четырьмя блюдами, — это конкурентность. Это способ **структурировать** работу.

_Diagram: Один работник, четыре задачи: он быстро переключается, и все движутся вперёд — но в каждый миг делается только одна._

#### Concurrency (конкурентность)

- **Проблема:** Задач много, а работник один — и он тратит время на ожидание.
- **Идея:** Переключаться между задачами, чтобы все двигались вперёд.
- **Пример:** Один повар ведёт четыре блюда, по шагу от каждого по очереди.
- **Цена:** Сложность: задачи переплетаются, и порядок шагов становится непредсказуем.
- **Связи:** Часто сочетается с **Parallelism**, но это другая идея. Основа для **Async** и **Event Loop**.

### Parallelism — параллелизм, много рук

> Четыре повара, каждый готовит своё блюдо в один и тот же миг.

**Parallelism** (параллелизм) — это когда много задач буквально работают в один миг, на нескольких работниках. Четыре повара — это параллелизм. Это способ **исполнять** работу. Concurrency и parallelism часто идут вместе, но это разные идеи: первое — про то, как разложить работу, второе — про то, сколько рук её делает.

_Diagram: Четыре ядра, четыре задачи — в один и тот же миг все четыре реально работают._

#### Parallelism (параллелизм)

- **Проблема:** Работы так много, что одного работника не хватает, даже если он не ждёт.
- **Идея:** Запустить задачи в один миг на нескольких работниках (ядрах).
- **Пример:** Большое вычисление, разбитое на куски, задействует каждое ядро машины.
- **Цена:** Нужно несколько ядер; общие данные между ними становятся опасны.
- **Связи:** Лекарство для **CPU-bound** работы. Опирается на **CPU Core**, **Thread** и **Process**.

### CPU Core — одно ядро конкурентно, много ядер параллельны

> Один кассир-билетёр может обслужить всю очередь, ведя быстрые шаги каждого по очереди; ряд кассиров обслуживает нескольких в одну и ту же секунду.

**Ядро** (core) процессора делает одно за раз, но может переключаться между задачами так быстро, что кажется, будто делает многое, — это конкурентность на одном ядре. Настоящему **параллелизму** нужно несколько ядер, реально работающих разом. У современных машин много ядер, так что реальный софт обычно делает оба: несколько задач в полёте (конкурентно), часть по-настоящему вместе (параллельно).

_Diagram: Внутри каждого ядра задачи чередуются (конкурентность); два ядра работают одновременно (параллелизм)._

#### CPU Core (ядро процессора)

- **Что это:** Один исполнитель в процессоре: в каждый миг делает одно дело.
- **Как работает:** Быстро переключается между задачами — поэтому кажется, что делает многое.
- **Пример:** Машина с 8 ядрами может по-настоящему делать 8 вещей в один миг.
- **Зачем знать:** Число ядер объясняет, какие ускорения вообще возможны.
- **Связи:** Одно ядро даёт **Concurrency**, много ядер — **Parallelism**. Переключение стоит **Context Switch**.

### Зачем вообще заморачиваться

> Официант, что принял полный заказ одного стола, отнёс его и лишь потом подошёл к следующему, оставил бы зал людей ждать впустую.

Делать одно за раз тратит огромные количества ожидания. Пока одна задача ждёт — сетевого ответа, диска, пользователя, — работник мог бы продвигать другую. Конкурентность отвоёвывает это простаивающее время; параллелизм добавляет сырую мощь под тяжёлую работу. Вместе они — вот почему приложение остаётся отзывчивым при загрузке и почему большое вычисление может задействовать каждое ядро. Цена — сложность, о которой весь остаток курса.

_Diagram: По очереди: работник простаивает, пока ждёт. Внахлёст: пока A ждёт, уже идёт B — и всё кончается раньше._

> **Вопрос рамки:** «Мне нужно, чтобы один работник перестал тратить время на ожидание (concurrency), или по-настоящему больше работников разом (parallelism) — и что моя задача на деле требует?»

> Concurrency — это жонглирование многими задачами через переключение; parallelism — запуск их в один миг на многих ядрах. Разные идеи: одна структурирует работу, другая её исполняет.

## 2. Потоки и процессы

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

### Process — процесс, изолированная программа

> Две отдельные кухни в двух отдельных зданиях — у каждой свои ингредиенты, свои столы, и они не могут тронуть запасы другой без звонка между ними.

**Процесс** (process) — это работающая программа со своей **приватной памятью**, изолированная от других процессов. Два процесса не могут случайно испортить данные друг друга, потому что не делят ничего, — чтобы общаться, они должны намеренно передавать сообщения. Эта изоляция делает процессы безопасными и крепкими, но тяжелее: запуск одного стоит больше, а разговор между ними медленнее.

_Diagram: У каждого процесса своя память. Общаются только сообщениями — безопасно, но дороже._

#### Process (процесс)

- **Проблема:** Одна программа может случайно испортить данные другой.
- **Идея:** Каждой программе — своя приватная память, отделённая стеной.
- **Пример:** Две запущенные программы на машине; общаются только сообщениями.
- **Цена:** Тяжелее: запуск стоит больше, разговор между процессами медленнее.
- **Связи:** Внутри процесса живут **Thread**. Общение процессов — это уже **Message Passing**.

### Thread — поток, делит память с собратьями

> Несколько поваров на одной кухне, делящих те же столы и ингредиенты, — быстро координировать, но они могут потянуться за тем же ножом в тот же миг.

**Поток** (thread) — это линия исполнения _внутри_ процесса, и потоки одного процесса **делят ту же память**. Это делает их лёгкими и быстрыми в координации — но это же и источник почти каждого бага конкурентности, потому что два потока могут тронуть одни данные в одно время. Общая память — это мощь и опасность: дешёвое общение, опасные столкновения.

_Diagram: Три потока в одном процессе смотрят в одну и ту же общую память._

#### Thread (поток)

- **Проблема:** Процессы тяжёлые, а частям одной программы надо тесно делить данные.
- **Идея:** Несколько линий исполнения внутри одного процесса, с общей памятью.
- **Пример:** Несколько поваров на одной кухне, у одних и тех же столов.
- **Цена:** Два потока могут тронуть одни данные разом — корень почти всех гонок.
- **Связи:** Общая память потоков — это **Shared Mutable State**, отсюда **Race Condition** и **Lock**.

### Context Switch — переключение контекста стоит времени

> Работник, жонглирующий делами, должен отложить одно, вспомнить точно, где он был, и взять другое, — и всё это откладывание и подхватывание берёт реальное время.

Когда одно ядро переключается с задачи на задачу, оно делает **переключение контекста** (context switch): сохраняет, где было, и грузит состояние следующей задачи. Это быстро, но не бесплатно, и делать это слишком часто — тысячи крошечных задач, молотящих, — тратит время в чистых накладных. Поэтому «просто сделай больше потоков» не автоматически быстрее.

_Diagram: Пара переключений — мелочь. Тысячи крошечных задач — и время уходит на само переключение._

#### Context Switch (переключение контекста)

- **Что это:** Ядро сохраняет состояние одной задачи и загружает состояние другой.
- **Пример:** Ядро чередует задачи A и B; между ними — короткая пауза на «сохранить и загрузить».
- **Подвох:** Слишком много задач — и само жонглирование стоит больше, чем экономит («молотилка»).
- **Вывод:** «Больше потоков» не значит автоматически «быстрее».
- **Связи:** Цена **Concurrency** на одном **CPU Core**; растёт с числом **Thread**.

> **Вопрос потоков:** «Этим задачам надо тесно делить данные (потоки, с риском столкновений) или оставаться безопасно изолированными (процессы, с накладными) — и не создаю ли я их столько, что доминирует переключение?»

> Процессы изолированы, с приватной памятью и безопасным-но-тяжёлым обменом сообщениями. Потоки делят память — лёгкие и быстрые, и корень большинства багов конкурентности.

## 3. Блокирующий и async

Бо́льшую часть времени программа не вычисляет — она ждёт. Как ты обращаешься с этим ожиданием, блокирующе или асинхронно, решает, может ли один работник оставаться занятым или сидит без дела.

### Blocking Call — блокирующий вызов останавливается и ждёт

> Стоять у стойки, пока клерк ушёл в подсобку за твоим заказом, не делая ничего до его возвращения, — ты не можешь помочь никому другому, пока стоишь там.

**Блокирующий** (blocking) вызов — это когда работник останавливается и ждёт результата, прежде чем делать что-либо ещё, — спросил базу и просто стоит, пока она не ответит. Пока заблокирован, тот работник не достигает ничего. Для одного быстрого шага это нормально; но если блокируешься на медленном — сеть, диск, другие сервисы, — ты тратишь ровно то время, что мог бы потратить на другую работу.

_Diagram: Работник отправил запрос и стоит без дела, пока не придёт ответ._

#### Blocking Call (блокирующий вызов)

- **Что это:** Вызов, на котором работник останавливается и ждёт результата.
- **Пример:** Спросил базу — и стоишь, пока она не ответит.
- **Когда нормально:** Один быстрый шаг: ждать почти нечего.
- **Цена:** На медленном (сеть, диск, другие сервисы) работник тратит время впустую.
- **Связи:** Противоположность **Async / Await**.

### Async / Await — один работник остаётся занятым

> Взять номерок в людной кулинарии: вместо того чтобы стоять замороженным, ты начинаешь другие дела и возвращаешься, когда твой номер выкликнут.

**Асинхронная** (asynchronous, неблокирующая) работа это переворачивает: ты начинаешь медленную операцию и вместо ожидания идёшь делать что-то ещё, подхватывая результат, когда он готов. Ключевые слова **async** и **await** во многих языках выражают ровно это — «начни это и дай мне продолжить; вернись, когда закончится». Один работник может иметь сотни медленных операций в полёте разом. Так один поток обслуживает тысячи ждущих соединений.

_Diagram: Работник запускает A, B и C, делает другую работу и забирает каждый результат, когда тот готов._

#### Async / Await (асинхронность, неблокирующая работа)

- **Проблема:** Блокирующий работник простаивает, пока ждёт медленное.
- **Идея:** Начать медленное, не ждать, а продолжить; вернуться, когда готово.
- **Пример:** Скрипт делает много сетевых вызовов: с async они идут вместе, а не один за другим.
- **Цена:** Код сложнее читать; помогает только там, где есть ожидание.
- **Когда брать:** **I/O-bound** работа: сеть, диск, другие сервисы.
- **Связи:** Обычно работает на **Event Loop**. Противоположность **Blocking Call**.

### Event Loop — цикл событий жонглирует ожиданием

> Единственный, очень организованный администратор, что принимает каждый звонок, запускает медленные поиски и берётся за того, кто готов следующим, — никогда не заблокированный ни на одном.

Async обычно работает на **цикле событий** (event loop): один работник, что начинает задачи, и всякий раз, когда медленное завершается, выполняет маленький кусок кода, что его ждал, и идёт дальше. Ощущается, будто много всего происходит разом, но это один работник, быстро переключающийся между готовыми задачами. Это модель за средами вроде **JavaScript** и **Node** — поразительно эффективными в жонглировании ожиданием, на одном потоке.

_Diagram: Цикл берёт следующую готовую задачу, запускает медленное и повторяет. Готовые ответы встают в очередь._

#### Event Loop (цикл событий)

- **Что это:** Один поток, который по кругу берёт следующую готовую задачу.
- **Как работает:** Запускает медленное; когда оно кончилось, выполняет маленький кусок кода, что его ждал.
- **Пример:** JavaScript и Node: тысячи ожиданий на одном потоке.
- **Подвох:** Тяжёлое вычисление занимает этот единственный поток — и все остальные ждут.
- **Связи:** Двигатель **Async / Await**; лучше всего для **I/O-bound** работы.

### I/O-bound vs CPU-bound — чего ждёт твоя работа

> Работе, что в основном ждёт доставок, помогает жонглёр получше; работе, что в основном тяжёлый подъём, нужно больше мускулов, а не лучшее расписание.

Правильный инструмент зависит от того, чего твоя работа ждёт. **I/O-bound** работа — в основном ждёт сети, диска или других сервисов — идеальна для async: один работник жонглирует всем ожиданием. **CPU-bound** работа — тяжёлое вычисление, что держит ядро полностью занятым, — не получает помощи от async (нет простаивающего времени отвоёвывать) и вместо этого нуждается в **параллелизме** по ядрам. Неверно оценить, что у тебя, — вот почему некоторые «ускорения» не делают ничего.

_Diagram: Слева работа в основном ждёт — её спасает async. Справа ядро всё время занято — нужны ещё ядра._

#### I/O-bound vs CPU-bound (упор в ожидание / упор в вычисления)

- **I/O-bound:** Работа в основном ждёт сети, диска, других сервисов → **async**.
- **CPU-bound:** Тяжёлое вычисление держит ядро занятым → **parallelism** по ядрам.
- **Пример:** Много сетевых вызовов — I/O-bound. Большое вычисление на всех ядрах — CPU-bound.
- **Подвох:** Перепутать — и «ускорение» не даёт ничего.
- **Связи:** Выбирает между **Async / Await** и **Parallelism**.

> **Вопрос async:** «Эта работа в основном ждёт (I/O-bound — тянись к async) или в основном вычисляет (CPU-bound — тянись к параллельным ядрам)? Использовать не то не купит ничего.»

> Блокирующий останавливается и ждёт; async начинает медленное и остаётся занятым. Async выигрывает для ожидания (I/O-bound); настоящий параллелизм — для тяжёлых вычислений (CPU-bound).

## 4. Проблема общего состояния

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

### Race Condition — состояние гонки: две руки, один предмет

> Два человека тянутся за последним печеньем в один миг — оба видят, что оно там, оба хватают, и что будет дальше, зависит от точного тайминга в доли секунды.

**Состояние гонки** (race condition) — это когда результат зависит от точного тайминга конкурентных операций, трогающих общие данные. Два потока оба читают счётчик как 5, оба прибавляют один, оба пишут 6 — и единица тихо теряется, потому что верный ответ был 7. Никто не сделал ошибки в логике; шаги просто переплелись плохо. Это определяющий баг конкурентности.

_Diagram: Оба потока прочитали 5, оба записали 6. Ждали 7 — одно обновление тихо потерялось._

#### Race Condition (состояние гонки)

- **Что это:** Результат зависит от точного тайминга задач над общими данными.
- **Пример:** Два потока читают счётчик 5, оба пишут 6 — а правильный ответ 7.
- **Причина:** **Общие** и **изменяемые** данные плюс одновременный доступ.
- **Подвох:** Ошибки в логике нет — шаги просто переплелись в плохом порядке.
- **Связи:** Растёт из **Shared Mutable State**. Лечится **Lock**, **Message Passing**, **Immutability**, **Atomic Operation**.

### Non-determinism — ужас в том, что он прерывист

> Дребезг в машине, что исчезает в тот миг, как ты привозишь её к механику, — реальный, прерывистый и невозможный показать по требованию.

Состояния гонки мерзки, потому что **недетерминированы** (non-deterministic): они зависят от тайминга, так что появляются раз в тысячу прогонов, никогда в твоих тестах и только под реальной нагрузкой в проде. Один и тот же ввод может сработать или упасть в зависимости от микроскопического расписания. Поэтому у багов конкурентности грозная репутация — их нельзя надёжно воспроизвести, и «у меня на машине работает» — ровно то, чего ждёшь, даже когда оно сломано.

_Diagram: Тот же код и тот же ввод: почти все прогоны проходят, а один падает — из-за тайминга._

#### Non-determinism (недетерминированность)

- **Что это:** Один и тот же ввод даёт разный результат в разных прогонах.
- **Почему:** Результат зависит от микроскопического расписания задач.
- **Пример:** Баг появляется раз в тысячу прогонов — никогда в тестах, только под нагрузкой в проде.
- **Подвох:** «У меня на машине работает» — ровно то, чего ждёшь, даже когда сломано.
- **Связи:** Делает **Race Condition** такой опасной. Поэтому баги лучше сделать невозможными, а не ловить.

### Shared Mutable State — общее изменяемое состояние

> Столкновения случаются только над тем, что обоим позволено хватать и менять, — запри общий шкаф или дай каждому свой, и драки прекратятся.

Заметь точный источник: данные, что одновременно **общие** (shared — больше одной задачи может дотянуться) и **изменяемые** (mutable — их можно поменять). Убери любое свойство — и гонка исчезает: данные, что трогает лишь одна задача, безопасны, а данные, что никто не меняет, безопасно делить свободно. Это ключевая мысль, на которой строится остаток курса: каждое лекарство — это способ управлять, ограничить или избежать общего изменяемого состояния.

_Diagram: Опасна только одна клетка из четырёх: данные и общие, и изменяемые._

#### Shared Mutable State (общее изменяемое состояние)

- **Что это:** Данные, к которым тянутся несколько задач и которые можно менять.
- **Идея:** Убери «общее» или «изменяемое» — и гонке негде жить.
- **Пример:** Общий счётчик или список, в который пишут два потока, — опасно. Данные одной задачи или неизменяемые — безопасно.
- **Вывод:** Каждое лекарство в курсе управляет, ограничивает или убирает это состояние.
- **Связи:** Источник **Race Condition**. **Lock** управляет им, **Message Passing** убирает «общее», **Immutability** убирает «изменяемое».

> **Вопрос гонки:** «Какой кусок данных две задачи могут менять в одно время здесь — и что будет, если их шаги переплетутся в худшем возможном порядке?»

> Состояние гонки — это две задачи, трогающие общие, изменяемые данные разом, с результатом, висящим на тайминге. Оно прерывисто, невидимо в тестах — и определяющий баг конкурентности.

## 5. Блокировки и deadlock

Классическое лекарство от гонки — сделать так, чтобы лишь одна задача трогала общие данные за раз. Оно работает — но инструмент, которым ты это насаждаешь, несёт собственную знаменитую опасность.

### Lock / Mutex — блокировка даёт одной задаче исключительный доступ

> Единственный ключ от туалета на одного: кто держит — заходит; все остальные ждут своей очереди снаружи. Двух человек внутри разом — никогда.

**Блокировка** (lock, или **mutex** — от «mutual exclusion», взаимное исключение) гарантирует, что лишь одна задача входит в участок кода за раз. Прежде чем тронуть общие данные, задача должна захватить блокировку; другие, желающие её, ждут, пока она не освободится. Защищённый участок — это **критическая секция** (critical section). Сделанное верно, это превращает хаотичную свалку над общими данными в упорядоченную очередь по-одному, и состояние гонки исчезает.

_Diagram: Поток A держит блокировку и работает в критической секции. Поток B ждёт снаружи._

#### Lock / Mutex (блокировка, взаимное исключение)

- **Проблема:** Две задачи меняют общие данные разом — гонка.
- **Идея:** Захватить блокировку → поработать в **критической секции** → отпустить. Остальные ждут.
- **Пример:** Поток берёт блокировку, прибавляет единицу к счётчику и отпускает её.
- **Цена:** Ожидание других задач; риск **Deadlock** и **Contention**.
- **Когда брать:** Когда общее изменяемое состояние правда не избежать.
- **Связи:** Управляет **Shared Mutable State**. Часто дешевле — **Atomic Operation** или **Message Passing**.

### Deadlock — взаимоблокировка: все ждут вечно

> Два человека у узкой двери, каждый отказывается отступить, пока не отступит другой, — оба застряли, вежливо, навсегда.

Блокировки несут свой классический сбой: **взаимоблокировку** (deadlock). Задача A держит блокировку 1 и хочет блокировку 2; задача B держит блокировку 2 и хочет блокировку 1. Каждая ждёт, пока другая отпустит, и ни одна не отпускает — обе замёрзли навсегда. Это идёт от захвата нескольких блокировок в разном порядке. Частое лекарство — дисциплина: всегда захватывай блокировки в одном согласованном порядке, чтобы **круговое ожидание** (circular wait) не смогло сложиться.

_Diagram: A держит 1 и хочет 2, B держит 2 и хочет 1 — круг замкнулся, обе ждут вечно._

#### Deadlock (взаимоблокировка)

- **Что это:** Задачи ждут друг друга по кругу, и ни одна не может продолжить.
- **Пример:** A держит блокировку 1 и хочет 2; B держит 2 и хочет 1.
- **Причина:** Несколько блокировок захватываются в разном порядке.
- **Лекарство:** Всегда брать блокировки в одном согласованном порядке.
- **Связи:** Классическая опасность **Lock / Mutex**.

### Lock Contention — блокировки верны, но дороги

> Один ключ от туалета держит порядок, но если он постоянно всем нужен, образуется очередь, и весь офис замедляется до скорости той единственной двери.

Блокировки чинят гонки, но у них есть цена: пока одна задача держит блокировку, другие ждут, так что тяжёлая блокировка может стереть скорость, ради которой ты вообще брал конкурентность. Чрезмерная блокировка создаёт **состязание** (contention) — все стоят в очереди на одну блокировку. Так что держи блокировки как можно короче, защищай как можно меньше и предпочитай конструкции, что требуют меньше блокировок. Верная программа, сериализованная на одной блокировке, немногим лучше однопоточной.

_Diagram: Четыре потока стоят в очереди к одной блокировке — работа идёт по одному._

#### Lock Contention (состязание за блокировку)

- **Что это:** Много задач стоят в очереди на одну и ту же блокировку.
- **Пример:** Программа, сериализованная на одной блокировке, — почти однопоточная.
- **Цена:** Стирает выигрыш от конкурентности: верно, но медленно.
- **Лекарство:** Держать блокировки коротко, защищать как можно меньше, выбирать конструкции с меньшим числом блокировок.
- **Связи:** Вторая цена **Lock / Mutex** после **Deadlock**.

> **Вопрос блокировки:** «Эта критическая секция настолько мала, насколько может быть, захватываю ли я блокировки в согласованном порядке, чтобы избежать deadlock, — и не стирает ли состязание мой выигрыш от конкурентности?»

> Блокировка даёт одной задаче за раз исключительный доступ, убивая гонку. Но блокировки могут уйти в deadlock, а чрезмерная блокировка сериализует всё — верно, но медленно.

## 6. Более безопасные паттерны

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

### Message Passing — не дели память, передавай сообщения

> Вместо многих поваров, дерущихся за один общий стол, каждый работает на своей станции и передаёт готовые тарелки по раздаче — двое никогда не тянутся за одним.

Мощная альтернатива общей памяти — **передача сообщений** (message passing): задачи не трогают одни данные; они шлют друг другу сообщения через **очередь** (queue). Лишь одна задача владеет данным куском, а другие просят изменений, посылая сообщение. Есть знаменитый лозунг: «не общайтесь, деля память; делите память, общаясь». Убери общий доступ — и убираешь гонку в корне, без всяких блокировок.

_Diagram: Задачи шлют сообщения в очередь. Данные трогает только задача-владелец._

#### Message Passing (передача сообщений)

- **Проблема:** Общая память требует блокировок и рождает гонки.
- **Идея:** Данными владеет одна задача; остальные просят изменений, посылая сообщения через очередь.
- **Пример:** Повара на своих станциях передают тарелки по раздаче, а не делят один стол.
- **Цена:** Сообщения идут медленнее, чем прямой доступ к памяти.
- **Когда брать:** Когда можно дать каждому куску данных одного владельца.
- **Связи:** Убирает «общее» из **Shared Mutable State**. Так общаются и **Process**.

### Immutability — неизменяемые данные нельзя загонять

> Вручить каждому свою ксерокопию вместо одного общего оригинала — они все могут читать разом, и никто не может черкнуть на том, что читает другой.

Если данные никогда не меняются после создания — если они **неизменяемы** (immutable), — то любое число задач может читать их в одно время с нулевым риском, потому что опасность была не в делении, а в делении _и изменении_. Вместо мутации значения на месте ты делаешь новое. Неизменяемость убирает одну половину условия «общее и изменяемое», а с ней — целую категорию багов. Поэтому функциональные стили так сильно на неё опираются.

_Diagram: Значение v1 не меняется — его читают сколько угодно задач. Изменение рождает новое значение v2._

#### Immutability (неизменяемость, immutable data)

- **Проблема:** Общие данные кто-то меняет, пока другие читают.
- **Идея:** Не менять значение на месте, а делать новое.
- **Пример:** Каждому — своя ксерокопия; черкнуть в чужой нельзя.
- **Цена:** Новые копии вместо правки на месте.
- **Когда брать:** Данные, которые многие читают. Опора функционального стиля.
- **Связи:** Убирает «изменяемое» из **Shared Mutable State**.

### Atomic Operation — атомарные операции неделимы

> Турникет, что считает каждого проходящего одним неделимым щелчком, — нет полусосчитанного мига, в который двое могут проскользнуть.

Некоторые операции **атомарны** (atomic): они случаются как один неделимый шаг, что нельзя прервать на полпути, так что две задачи не могут застать друг друга в середине обновления. Языки и базы дают **атомарные счётчики**, **атомарные обмены** и **транзакции** ровно для тех маленьких общих обновлений, что любят гонки. Когда тебе правда надо делить меняющееся значение, атомарная операция часто дешевле и безопаснее полной блокировки.

_Diagram: «Прочитать, прибавить, записать» — три шага, и между ними может влезть другая задача. Атомарный инкремент — один шаг._

#### Atomic Operation (атомарная операция)

- **Проблема:** Обновление из нескольких шагов может прерваться на середине.
- **Идея:** Сделать обновление одним неделимым шагом.
- **Пример:** Атомарный счётчик, атомарный обмен, транзакция в базе.
- **Цена:** Подходит только для маленьких обновлений, не для большой логики.
- **Когда брать:** Общее меняющееся значение, вроде счётчика.
- **Связи:** Часто дешевле и безопаснее **Lock / Mutex**; чинит **Race Condition**.

> **Вопрос паттерна:** «Могу ли я избежать общего изменяемого состояния тут целиком — передав сообщение, сделав данные неизменяемыми или использовав одну атомарную операцию — вместо того чтобы тянуться к блокировке?»

> Лучший баг конкурентности — сделанный невозможным: передавай сообщения вместо деления, делай данные неизменяемыми или используй атомарные операции — и гонке негде жить.

## 7. Применять хорошо

Конкурентность — это мощь, что стоит добавлять нарочно, а не рефлексом. Навык — тянуться к ней, только когда задача требует, и выбирать простейшую модель, что делает дело.

### Подбери инструмент под узкое место

> Ты не нанимаешь больше грузчиков на работу, что вся ожидание, и не берёшь лучшего диспетчера на работу, что вся тяжёлый подъём, — ты подбираешь помощь туда, куда реально уходит время.

Прежде чем добавлять конкурентность, знай своё **узкое место** (bottleneck). Если работа **I/O-bound** — в основном ожидание — async на одном потоке часто решает это просто. Если она **CPU-bound** — в основном вычисление — нужен настоящий **параллелизм** по ядрам. Если ни то ни другое — быстро и просто — добавление конкурентности просто покупает баги без выигрыша. Самая частая ошибка — тянуться к потокам, когда обычный последовательный код был уже достаточно быстр.

_Diagram: Сначала спроси, куда уходит время. Ожидание → async, вычисления → параллелизм, ни то ни другое → оставь как есть._

### Держи общую поверхность крошечной

> Мастерская, где каждый в основном работает один, с одной маленькой, ясно помеченной общей полкой инструментов, — чем меньше общего, тем меньше может пойти не так.

Раз каждый баг конкурентности идёт от общего изменяемого состояния, главный ход — **минимизировать деление**. Держи бо́льшую часть данных во владении одной задачи, делай что можешь неизменяемым и ограничь по-настоящему общие, меняющиеся части маленькой, тщательно охраняемой поверхностью. Конструкцию с крошечной общей поверхностью можно реально осмыслить; ту, где всё общее и изменяемое, — это дом с привидениями. Простота тут не опциональна — это вся защита.

### Понятия связаны между собой

> Генеалогическое древо: у всех бед одна прародительница, а у всех лекарств — одна цель.

Почти всё в курсе крутится вокруг одной точки. **Потоки** делят память → появляется **общее изменяемое состояние** → оно рождает **гонки**. Четыре лекарства — **lock**, **message passing**, **immutability**, **atomic** — управляют им или убирают его. У блокировок свои беды: **deadlock** и **contention**. Отдельно стоит выбор под нагрузку: ожидание → **async + event loop**, вычисления → **parallelism**.

_Diagram: Главные связи между понятиями курса._

#### Прежде чем добавить конкурентность

- **Какое узкое место** — I/O-bound (async), CPU-bound (параллелизм) или ни то ни другое (не надо)?
- **Какие данные общие и изменяемые**, и могут ли две задачи тронуть их разом?
- **Могу ли я избежать деления** — передачей сообщений, неизменяемостью или одной атомарной операцией?
- **Если надо блокировать**, мала ли критическая секция и согласован ли порядок блокировок?
- **Может ли уйти в deadlock**, и исключил ли я круговое ожидание?
- **Стоит ли добавленная сложность того**, или последовательный код был уже достаточно быстр?

#### Признаки, что ты справляешься хорошо

- Ты тянешься к **async** для ожидания и к **параллелизму** для вычислений — не рефлексом.
- Можешь указать ровно, какие данные **общие и изменяемые**, и их мало.
- Ты предпочитаешь **передачу сообщений и неизменяемость** разбрасыванию блокировок повсюду.
- Твои блокировки **короткие, упорядоченные**, и ты порассуждал о **deadlock**.
- Ты не добавляешь конкурентность там, где обычный **последовательный** код был уже достаточно быстр.

> Конкурентность — намеренная мощь: async для ожидания, параллелизм для вычислений и крошечная общая поверхность, охраняемая нарочно. Каждый баг восходит к общему изменяемому состоянию — так дели как можно меньше.

## Шпаргалка: все 19 понятий

### Основы

- **Concurrency** (конкурентность): Один работник жонглирует задачами, переключаясь. Способ структурировать работу.
- **Parallelism** (параллелизм): Много задач в один миг на многих ядрах. Способ исполнять работу.
- **CPU Core** (ядро процессора): Делает одно за раз; одно ядро — конкурентность, много — параллелизм.

### Потоки и процессы

- **Process** (процесс): Изолированная программа со своей памятью. Безопасно, но тяжело.
- **Thread** (поток): Линия исполнения внутри процесса; потоки делят память. Быстро и опасно.
- **Context Switch** (переключение контекста): Сохранить одну задачу, загрузить другую. Не бесплатно.

### Ожидание

- **Blocking Call** (блокирующий вызов): Остановиться и ждать ответа, ничего не делая.
- **Async / Await** (асинхронность): Начать медленное, продолжить работу, вернуться, когда готово.
- **Event Loop** (цикл событий): Один поток по кругу берёт следующую готовую задачу.
- **I/O-bound vs CPU-bound** (ожидание / вычисления): Ждёт → async. Считает → параллелизм.

### Общее состояние

- **Race Condition** (состояние гонки): Результат зависит от тайминга задач над общими данными.
- **Non-determinism** (недетерминированность): Тот же ввод — разный результат. Баг редко виден в тестах.
- **Shared Mutable State** (общее изменяемое состояние): Источник всех гонок. Убери «общее» или «изменяемое».

### Блокировки

- **Lock / Mutex** (блокировка): Одна задача за раз в критической секции.
- **Deadlock** (взаимоблокировка): Задачи ждут друг друга по кругу. Лекарство — один порядок блокировок.
- **Lock Contention** (состязание): Очередь на одну блокировку стирает скорость.

### Более безопасные паттерны

- **Message Passing** (передача сообщений): У данных один владелец; остальные шлют ему сообщения.
- **Immutability** (неизменяемость): Данные не меняются — читать могут все разом.
- **Atomic Operation** (атомарная операция): Обновление одним неделимым шагом.

Конец экспресс-курса · 7 глав · 19 понятий

Дальше — практика: возьми что-то, что много ждёт, — скрипт, делающий много сетевых вызовов один за другим, — и сделай вызовы async, чтобы они шли вместе. Смотри, как он заканчивается за долю времени. Потом найди общий счётчик или список и порассуждай, что будет, если две задачи ударят в него разом. Идеи становятся конкретными в тот миг, как ты чувствуешь ускорение и замечаешь гонку. Но держи одну мысль выше прочих: делать много вещей разом — это как софт остаётся быстрым, и каждая сложная часть восходит к тому же корню — две вещи трогают одни данные в одно время. Управь этим — и ты приручил конкурентность.
