Конкурентность и параллелизм. Конкурентность — способность вести много задач, чередуя их; параллелизм — реально выполнять их одновременно на нескольких ядрах. Асинхронность даёт конкурентность, потоки — и то, и другое (кроме Python с GIL).
Потоки. Каждый поток — отдельный стек и сущность ядра. Пока поток ждёт сеть, ядро отдаёт процессор другому. Модель простая: код пишется последовательно. Ограничение — цена: память на стек, переключения через ядро. Тысячи потоков — нормально, сотни тысяч — уже нет.
Event loop. Один поток крутит цикл: спросить у ОС, какие сокеты готовы (epoll, kqueue), и продолжить задачи, которые их ждали. Задача выполняется до точки ожидания (await), там сама отдаёт управление — кооперативная многозадачность. Ожидающая задача — это небольшой объект в памяти, а не поток, поэтому их могут быть сотни тысяч. Так работают Node.js, nginx, asyncio в Python, Netty и WebFlux.
Цена асинхронности:
- заразность —
async функцию можно вызвать только из async; код делится на два «цвета»; - блокирующий вызов (синхронный драйвер базы,
time.sleep, requests.get, тяжёлый цикл) останавливает весь event loop и все задачи. Такие вызовы выносят в пул потоков (run_in_executor); - сложнее отладка и стектрейсы, нужно следить за отменой и таймаутами.
Когда что:
- много одновременных соединений, в основном ожидание — асинхронность (шлюзы, чаты, вебсокеты, прокси, скрейпинг);
- вычисления — процессы или потоки по числу ядер;
- обычный CRUD-сервис на сотни RPS — потоки проще и достаточно.
Лучшее из двух миров. Горутины в Go и виртуальные потоки Java 21: код пишется как обычный блокирующий, а рантайм при ожидании снимает лёгкий поток с потока ОС и ставит другой. Миллионы таких потоков возможны. Ограничения: в Java долго была проблема «прилипания» (pinning) при synchronized, исправленная в Java 24; и ресурсы вроде пула соединений к базе по-прежнему нужно ограничивать.