Практикум — последовательное и параллельное выполнение
Продолжение живых примеров — тот же сценарий в коде на разных языках. Сначала один учебный кейс, затем четыре способа его ускорить и таблица выбора инструмента.
Теория процессов и потоков — Процессы и потоки. Event loop и async/await — Асинхронное выполнение.
Словарь перед практикумом
| Термин | Значение | Где подробнее |
|---|---|---|
| Последовательное выполнение | Задача B стартует только после полного завершения задачи A | Синхронность и асинхронность |
| I/O-bound | Программа в основном ждёт сеть, диск или БД; CPU свободен | Живые примеры |
| CPU-bound | Программа считает — циклы, шифрование, обработка картинок | Параллельные вычисления |
| Поток (thread) | Цепочка команд внутри процесса; потоки делят память | Управление потоками |
| Процесс | Отдельная программа со своей памятью; обмен через IPC | Процессы и потоки |
| Асинхронность | Запустили ожидание и перешли к другой работе в том же потоке | Асинхронное выполнение |
| Event loop | Цикл, который крутит задачи, пока ждут I/O (JS, Python asyncio) | JavaScript 21 |
| GIL | Глобальная блокировка интерпретатора CPython — один поток выполняет байт-код | Python 29 |
| Гонка данных (race condition) | Два потока меняют одну переменную без синхронизации — результат случаен | Управление потоками |
join() | Главный поток ждёт завершения дочернего | thread-lifecycle-demo |
Учебная задача — скачать несколько страниц
Реальный HTTP в примерах заменён на sleep — так проще увидеть разницу во времени без сети и сервера.
| Страница (учебный URL) | Имитация задержки сети |
|---|---|
https://example.com/page1 | 2.0 с |
https://example.com/page2 | 3.5 с |
https://example.com/page3 | 1.5 с |
https://example.com/page4 | 2.5 с |
https://example.com/page5 | 1.0 с |
При последовательном запуске задержки складываются — около 10.5 с.
При параллельном (потоки, asyncio, горутины) все пять "загрузок" идут одновременно — время близко к 3.5 с (самая долгая страница).
Последовательно: [====page1====][======page2======][==page3==]... → ~10.5 с
Параллельно: [====page1====]
[======page2======] → ~3.5 с
[==page3==]
[====page4====]
[=page5=]
В продакшене вместо sleep будет HTTP-запрос — логика выбора инструмента та же.
Python — четыре режима в одной программе
Полный листинг с прогресс-баром (tqdm), блоками I/O и CPU и ASCII-графиком — в code.spirzen.ru:
Что сравнивает программа
| Режим | I/O (15 "загрузок") | CPU (расчёты) | Краткий разбор |
|---|---|---|---|
| Последовательно | ~25 с | очень долго | Каждая задача ждёт предыдущую |
threading / ThreadPoolExecutor | ~3–5 с | почти без ускорения | На I/O поток отпускает GIL |
asyncio | ~3–5 с | ускорения нет | Один поток, много одновременных ожиданий |
multiprocessing | обычно избыточно | ускорение по ядрам | У каждого процесса свой интерпретатор и свой GIL |
В стандартном CPython в один момент времени байт-код исполняет один поток. Потоки хорошо работают, когда программа ждёт сеть или диск. Для тяжёлых вычислений на CPU берут multiprocessing или ProcessPoolExecutor — см. раздел multiprocessing.
Разбор по шагам и короткие фрагменты — Практикум в статье про Python. Основы asyncio — 291.
Java — потоки, пул, гонки, синхронизация
В Java наглядно видны жизненный цикл потока, гонка данных на общем счётчике и синхронизация на примере банковского счёта.
Блоки демо-программы
- Простые потоки —
Thread.start(),join(), чередование строк в консоли. - Гонка данных — десять потоков делают
sharedCounter++без защиты; ожидали 10 000, получают меньше. - Синхронизация — методы
BankAccountпомеченыsynchronized; баланс остаётся корректным. - Runnable — одна задача, два потока с разными именами.
- Пул потоков —
ExecutorServiceпереиспользует потоки вместоnew Threadна каждую задачу.
Интерактив — жизненный цикл потока (аналог на Python threading).
API и выбор между platform threads и virtual threads — Асинхронность в Java. Память JVM и synchronized — JVM и потоки.
C# — async, Task и Parallel
В .NET разные инструменты закрывают разные виды ожидания и вычислений.
async/await— поток освобождается на время I/O (HTTP, БД, файл).Task.WhenAll— несколько независимых асинхронных операций стартуют сразу.Task.Run— тяжёлая CPU-работа уходит в пул потоков.Parallel.ForEach— цикл распределяется по ядрам CPU.
| Ситуация | Подходящий API |
|---|---|
| HTTP, БД, файлы | async/await, Task.WhenAll |
| Долгий расчёт в окне WPF/MAUI | Task.Run + Dispatcher / MainThread |
| Параллельная обработка списка файлов | Parallel.ForEach |
| Общий счётчик из нескольких потоков | lock, Interlocked, ConcurrentDictionary |
Подробнее — статья про async и многопоточность в C#. Класс Thread — 391, Task — 392.
JavaScript и TypeScript — event loop и worker_threads
В браузере и в Node.js JavaScript-код в одном потоке исполняется по одной инструкции. Пока ждём ответ сервера, event loop выполняет другие колбэки и микрозадачи (Promise).
Где какой инструмент
| Среда | Много сетевых запросов | Тяжёлые вычисления на CPU |
|---|---|---|
| Браузер | fetch, Promise.all | Web Worker |
| Node.js | async/await, fetch | модуль worker_threads или child_process |
TypeScript типизирует Promise<T> — компилятор проверяет результат Promise.all как T[], без any.
Go — горутины и WaitGroup
Горутина — лёгкая задача, которую runtime Go ставит на потоки ОС. Ключевое слово go запускает функцию параллельно; sync.WaitGroup ждёт завершения всех "загрузок".
Планировщик Go (модель G–M–P) может держать сотни тысяч горутин на небольшом числе потоков ядра. Каналы и правило CSP — Асинхронность и горутины. Гонки — Механика языка.
PHP — запрос за запросом и параллельный cURL
Классическая схема PHP-FPM
- один входящий HTTP-запрос;
- один процесс (или воркер);
- код в скрипте идёт сверху вниз.
Если внутри скрипта пять раз вызвать file_get_contents в цикле, задержки суммируются. Параллельные исходящие HTTP в одном скрипте — через curl_multi_*.
С PHP 8.1 есть встроенные Fibers. Для долгоживущих воркеров и высокой нагрузки — Swoole, RoadRunner, ReactPHP — обзор в экосистеме PHP.
Практикум в разделе PHP — Параллельные HTTP-запросы. Модель "запрос — ответ" — Что такое PHP.
Kotlin — корутины и async / awaitAll
Корутина — лёгкая задача на JVM: тысячи корутин дешевле тысячи потоков ОС. suspend-функция при delay или HTTP отдаёт поток другим корутинам.
Пачка "загрузок" — async внутри coroutineScope и awaitAll():
Корутины в Kotlin. Сравнение с Java — 298, Java и Kotlin.
Groovy на JVM — потоки Java и GPars
Groovy компилируется в байт-код JVM и использует те же java.util.concurrent API, что и Java. Библиотека GPars добавляет параллельные коллекции и Fork/Join-подобные конструкции.
Практикум на JVM. Потоки Java — Асинхронность в Java.
Типичный вывод Python-программы (ASCII-график)
I/O задачи (15 загрузок):
sequential (IO) ████████████████████████████████████████████ 25.35 с
threads (IO) ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 4.82 с
async (IO) ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 6.15 с
CPU задачи:
sequential (CPU) ████████████████████████████████████████████ 45.00 с
processes (CPU) ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 12.40 с
threads (CPU) ██████████████████████████████████████████░ 43.80 с
- I/O — ускорение потоков и
asyncioотносительно последовательного кода примерно в 3–5 раз. - CPU — процессы дают выигрыш близкий к числу ядер; потоки в CPython для CPU почти не ускоряют из-за GIL.
Сводная таблица по языкам
| Язык | I/O-bound | CPU-bound | Статья |
|---|---|---|---|
| Python | asyncio, threading | multiprocessing | 29 |
| Java | virtual threads, CompletableFuture | ExecutorService, ForkJoinPool | 298 |
| C# | async/await | Parallel, Task.Run | 39 |
| JavaScript | Promise, event loop | worker_threads | 21 |
| TypeScript | те же примитивы + типы Promise<T> | те же + типизация worker | 17 |
| Go | горутины, каналы | горутины на все ядра | 21 |
| PHP | curl_multi, Swoole, Fibers | отдельные воркеры | 10 |
| Kotlin | корутины | Dispatchers.Default | 222 |
| Groovy | ExecutorService, GPars | GPars parallel collections | 11 |
Итоги
- Определите тип нагрузки — I/O-bound или CPU-bound.
- Для I/O используйте async, потоки (где GIL не мешает), горутины или корутины — главное, чтобы один поток не простаивал в ожидании сети.
- Для CPU — процессы или пул с числом воркеров около числа ядер; в Python для CPU потоки почти не дают ускорения.
- При общей памяти между потоками нужны
lock,synchronized, атомики — иначе гонки. - Для изоляции плагинов и фоновых задач — отдельный процесс или очередь сообщений.
Что читать дальше
| Тема | Статья |
|---|---|
| Сценарии без кода (сайт, десктоп, сервер) | Асинхронность простым языком |
| Event loop, корутины, колбэки | Асинхронное выполнение |
| Мьютексы, deadlock | Управление потоками |
| MPI, OpenMP, ускорение на кластере | Параллельные вычисления |
| Каталог embed-примеров | IT Code Examples |