Перейти к основному содержимому

Асинхронность простым языком — живые примеры

Разработчику Архитектору

Если вы прочитали процессы и потоки и асинхронное выполнение, но всё равно не понимаете зачем это в коде — начните отсюда. Ниже — не определения из учебника, а ситуации из реальных программ: что ломается без асинхронности, что именно "делится" на потоки, и какой будет результат.

Маршрут по разделу

1. Эта статья — карта местности.
2. Процессы и потоки — как устроено в ОС.
3. Асинхронное выполнение — event loop, async/await, колбэки.
4. Управление потоками — мьютексы, гонки, deadlock.
5. IPC — когда программы общаются как отдельные процессы.


Главная путаница в том, что это не одно и то же

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

ЧтоПростыми словамиКогда братьАналогия
Синхронный кодШаг 1 → ждёшь → шаг 2Прототип, 3 строки, скрипт "раз в день"Официант стоит у кухни, пока не принесут блюдо
АсинхронностьЗапустил ожидание → делаешь другое → вернулся по callback/awaitСеть, диск, БД, тысячи клиентов на одном сервереОфициант принял заказ и пошёл к другому столу
ПотокиНесколько цепочек команд в одном процессе, общая памятьUI + тяжёлый расчёт, пул worker'ов, CPU на ядрахНесколько официантов в одном зале
ПроцессыОтдельные программы, разная память, обмен через IPCПлагины, вкладки, изоляция сбоевКухня ресторана и курьерская служба — разные фирмы

Хороший пример сразу: пользователь нажал "Загрузить отчёт" в браузере.

Синхронно (плохо): кнопка → ждём сервер 2 сек → экран мёртвый
Async (хорошо): кнопка → fetch() → спиннер → клики работают → пришёл JSON → таблица
Поток (если CPU): кнопка → worker считает 10 сек → UI живой
Процесс (изоляция): редактор → child-процесс для плагина → упал плагин, редактор жив

Важно: асинхронность не обязана означать "много потоков". В Node.js и в браузере основной JavaScript-код часто крутится в одном потоке, а "параллельность" — это пока ждём сеть или диск, event loop выполняет другие задачи в том же потоке.

Ниже — три интерактивных демо на реальном коде. Запустите «Пошагово» и смотрите, что делает ОС или runtime на каждой строке.

Поток — создание, start, join, общая память

Python threading: один процесс, переменная balance общая, Lock защищает от гонки. Обратите внимание: Thread(...) ещё не поток ОС — поток появляется только на start().

Процесс — изоляция и очередь сообщений

Python multiprocessing: у child своя память, результат — только через Queue. Так устроены вкладки Chrome и worker-процессы на сервере.

Асинхронность — Promise и fetch без новых потоков

Тот же сценарий "загрузить ленту", но в JavaScript: fetch уходит в Web API, console.log('B') выполняется до ответа сервера. Подробнее про Promise — в асинхронности JavaScript.


Зачем это нужно — одна формула

Не давай программе простаивать, пока она ждёт что-то медленное.

Медленное — это почти всегда не процессор, а внешний мир:

  • ответ сервера по сети (50–500 мс и больше);
  • чтение файла с диска;
  • запрос к базе данных;
  • ожидание нажатия кнопки пользователем;
  • таймер "подождать 3 секунды".

Процессор за это время свободен. Асинхронность и многопоточность — способы заполнить это время полезной работой или не морозить интерфейс.


Живой пример 1 — сайт грузит ленту новостей

Сценарий: пользователь открыл страницу. Нужно: показать шапку → запросить посты с сервера → отрисовать список.

Плохо (синхронно, в лоб)

1. Нарисовать шапку
2. Отправить HTTP-запрос на сервер
3. СТОЯТЬ И ЖДАТЬ ответ 2 секунды — экран белый, ничего не кликается
4. Нарисовать посты

Пользователь думает, что сайт завис.

Хорошо (асинхронно)

1. Нарисовать шапку и спиннер "Загрузка…"
2. Отправить HTTP-запрос (не ждём в том же месте кода)
3. Пока ждём — обрабатываем клики, анимацию, другие запросы
4. Пришёл ответ → убрать спиннер → нарисовать посты

Что "разделили"? Не потоки — моменты времени. Запрос ушёл в фон (ОС + сетевая подсистема), а логика UI продолжила жить. В JavaScript это fetch(...).then(...) или await fetch(...).

Когда нужны потоки здесь? Обычно нет. Достаточно async I/O. Поток понадобится, если на клиенте нужно тяжело считать миллион записей (сжатие, ML) — тогда Web Worker или worker thread.


Живой пример 2 — кнопка : Экспорт в Excel в десктоп-программе

Сценарий: бухгалтер нажал кнопку. Программа формирует отчёт на 100 000 строк — 10 секунд CPU.

Плохо

Вся обработка в UI-потоке (главном потоке окна):

Нажали кнопку → считаем 10 секунд → окно не двигается, "Не отвечает"

Windows/macOS помечают программу как зависшую.

Хорошо

UI-поток: нажали кнопку → показали прогресс-бар → создали фоновый поток/worker
Фоновый поток: считаем 10 секунд
UI-поток: по готовности — обновить таблицу (через безопасный вызов в UI)

Что "разделили"? Два потока в одном процессе: один рисует окно, второй считает. Память общая — поэтому результат передают через очередь или Invoke/Dispatcher, а не просто "записали в переменную из двух мест" (иначе гонка данных).

Это не "асинхронность ради сети" — это вынос CPU-работы с UI-потока. Инструменты: Task.Run в C#, threading.Thread в Python, worker_threads в Node.js.


Живой пример 3 — веб-сервер и 1000 пользователей

Сценарий: API-сервер. Каждый запрос: проверить токен → сходить в БД → вернуть JSON. Запрос к БД — 20 мс ожидания.

Плохо — поток на каждый запрос

Пришёл запрос 1 → создали поток → ждём БД
Пришёл запрос 2 → создали поток → ждём БД
...
Пришёл запрос 5000 → "too many threads", сервер падает

Потоки дорогие (память на стек, переключение контекста).

Хорошо — async I/O + ограниченный пул

Один поток (или небольшой пул) принимает запросы
На каждый запрос: await database.query() — поток освобождается для других
Пока 1000 запросов ждут БД, сервер обслуживает новые

Результат: 1000 "висящих" запросов к БД — нормально; 1000 OS-потоков — катастрофа.

ПодходКогда уместен
async/await, event loopМного одновременных сетевых/БД-ожиданий, мало CPU на запрос
Пул из N потоковЕсть блокирующие библиотеки без async API
Отдельный процесс на сервисИзоляция, падение одного не валит всё (микросервисы)

Живой пример 4 — обработка 10 000 фотографий

Сценарий: уменьшить размер каждого JPEG. Это CPU-bound — процессор занят, диск почти не ждём.

Async тут почти не поможет

Один поток с async всё равно последовательно жжёт CPU на каждой картинке. "Пока одна картинка считается" — некому переключиться на другую в том же потоке, если работа чисто вычислительная.

Нужна параллельность по ядрам

4 ядра CPU → 4 потока/worker'а
Каждый берёт следующую фото из очереди → ресайз → кладёт результат
Очередь: [фото1, фото2, фото3, … фото10000]

Поток 1: фото1 → фото5 → фото9 …
Поток 2: фото2 → фото6 → …
Поток 3: фото3 → …
Поток 4: фото4 → …

Что "разделили"? Задачи (файлы), а не "этапы одного запроса". Общая очередь + пул потоков или multiprocessing в Python (обход GIL для CPU).

Подробнее про ускорение расчётов — Параллельные вычисления.


Живой пример 5 — Chrome и : убить вкладку

Сценарий: одна вкладка зависла на бесконечном цикле JavaScript.

Почему остальные вкладки живы? Каждая вкладка (упрощённо) — отдельный процесс с изолированной памятью. Зависла одна — диспетчер задач показывает "Chrome (вкладка X)", можно убить только её.

Что "разделили"? Процессы, не потоки. Обмен между ними — через IPC (сокеты, каналы), а не через общие переменные.

ЗадачаПроцессыПотокиAsync в одном потоке
Не дать упасть всему из-за одного модуля
Быстро делить данные внутри одной программычастично
Тысячи одновременных HTTP-запросовизбыточно
Ресайз 10k фото на 8 ядрахможно

Как это выглядит в коде — один сценарий, три способа

Задача: скачать два файла с интернета и показать "Готово".

Синхронно (медленно, но понятно)

# Псевдокод — так НЕ делают в UI и на сервере с нагрузкой
data_a = download("https://example.com/a.zip") # ждём 3 сек
data_b = download("https://example.com/b.zip") # ещё 3 сек
# Итого ~6 сек, всё время программа "стоит"
show("Готово")

Асинхронно (I/O-bound, один поток)

# Псевдокод asyncio
async def main():
task_a = download_async("https://example.com/a.zip")
task_b = download_async("https://example.com/b.zip")
data_a, data_b = await gather(task_a, task_b)
# Оба запроса шли параллельно по сети → ~3 сек
show("Готово")

Что произошло: оба запроса отправились, пока сеть думала — event loop не спал. Потоков OS могло быть один.

Два потока (тоже ~3 сек, но другая модель)

# Псевдокод threading
results = {}
def worker(url, key):
results[key] = download(url) # блокирующий вызов

start_thread(worker, url_a, "a")
start_thread(worker, url_b, "b")
wait_all_threads()
show("Готово")

Результат по времени похож, но модель другая: два системных потока, нужно следить за results (мьютекс, если пишем в общую структуру).


Практикум — : что выбрать? — 6 ситуаций

Ответьте себе на два вопроса:

  1. Программа в основном ждёт (сеть, диск, БД) или считает (циклы, картинки, ML)?
  2. Нужно ли не блокировать окно / обслуживать ещё 500 клиентов?
СитуцияЖдём или считаем?Что делатьПочему
1Форма логина, запрос к APIЖдём сетьasync/await, спиннерUI не должен зависать
2Чат в браузере, новые сообщенияЖдём сетьWebSocket + event loopПостоянный канал, не polling в цикле
3Генерация PDF на 50 страниц в WinFormsСчитаем CPUФоновый Task/поток + прогрессAsync не ускорит чистый CPU в одном потоке
4Парсинг 1 млн строк CSVСчитаем + дискПотоки или процессы + очередь порцийПараллелим порции данных
5Плагин от сторонней команды в редактореИзоляцияОтдельный процессКраш плагина не убивает редактор
6"Отправить email после оплаты"Ждём внешний сервисОчередь сообщений (RabbitMQ, SQS)Пользователю не ждать SMTP; IPC

Практикум пошагово — : интернет-магазин, кнопка Оформить заказ

Разберём, что где выполняется, без магии.

По шагам:

  1. Браузер: await fetch('/order') — не блокирует отрисовку; пользователь может скроллить.
  2. API: await db.insert() — поток сервера не простаивает, принимает другие запросы.
  3. API не шлёт email сам 30 секунд — кладёт задачу в очередь и сразу отвечает "принято".
  4. Worker-процесс в фоне делает склад и почту. Упал worker — API жив; сообщения лежат в очереди.

Где "разделили":

  • UI и сеть в браузере — async;
  • тяжёлая фоновая работа — отдельный процесс (worker);
  • согласование — очередь сообщений, не общая переменная.

Когда НЕ надо плодить потоки и процессы

  • CRUD-сайт на 50 пользователей — хватит одного сервера и async.
  • Скрипт "раз в день выгрузить отчёт" — один поток, проще отладить.
  • "Сделаю 100 потоков для скорости" на задаче с общей переменной без lock — получите гонки и баги раз в десять запусков.

Правило: сначала измерь, где тормоз (сеть? диск? CPU?). Потом выбирай инструмент из таблицы выше.


Частые ошибки понимания

МифКак на самом деле
"Асинхронность = всегда быстрее"Быстрее отклик и пропускная способность при ожидании I/O; чистый CPU одним async не ускоришь
"Async значит второй поток пошёл работать"Часто нет — один поток, другая задача в очереди event loop
"Многопоточность = асинхронность"Потоки — про параллельное выполнение; async — про не блокировать текущую цепочку
"Разделил на процессы — данные общие"Нет. Нужен pipe, файл, очередь, HTTP — IPC
"Поставлю mutex везде — будет безопасно"Можно словить deadlock и падение производительности

Мини-шпаргалка — симптом → решение

СимптомВероятная причинаКуда копать
Окно "Не отвечает" при нажатии кнопкиДолгая работа в UI-потокеФоновый поток / Task.Run / worker
Сервер тупит при 100+ клиентахБлокирующий I/O или поток на запросasync API, connection pool
Счётчик "теряет" единицыГонка данныхmutex, atomic, immutable данные
Вкладка/плагин валит всё приложениеНет изоляцииотдельный процесс
Два заказа на один товарПараллельные запросы без транзакцииБД + lock, не только async UI

Больше разборов ошибок — FAQ раздела.


Что читать дальше

ВопросСтатья
Как устроены процесс, поток, планировщикПроцессы и потоки
Event loop, Promise, async/awaitАсинхронное выполнение
Мьютекс, deadlock, очереди задачУправление потоками
Очереди, RabbitMQ, WebSocket между сервисамиIPC
Ускорение расчётов на многих ядрахПараллельные вычисления

Итог в одном абзаце: асинхронность нужна, когда программа ждёт сеть, диск или пользователя — чтобы не стоять столбом. Потоки нужны, когда есть несколько независимых цепочек работы (UI + расчёт, несколько ядер CPU). Процессы нужны, когда нужна изоляция и отказоустойчивость. В реальных проектах это комбинируют: async в API, worker-процесс для фона, потоки для UI — и это нормально.

Содержание
Освоение главы0%