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

Практикум — последовательное и параллельное выполнение

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

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

Теория процессов и потоков — Процессы и потоки. 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/page12.0 с
https://example.com/page23.5 с
https://example.com/page31.5 с
https://example.com/page42.5 с
https://example.com/page51.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
GIL (Global Interpreter Lock)

В стандартном CPython в один момент времени байт-код исполняет один поток. Потоки хорошо работают, когда программа ждёт сеть или диск. Для тяжёлых вычислений на CPU берут multiprocessing или ProcessPoolExecutor — см. раздел multiprocessing.

Разбор по шагам и короткие фрагменты — Практикум в статье про Python. Основы asyncio291.


Java — потоки, пул, гонки, синхронизация

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

Пример кода загружается. Это пример интеграции — сейчас мы запрашиваем данные из другой системы.

Блоки демо-программы

  • Простые потокиThread.start(), join(), чередование строк в консоли.
  • Гонка данных — десять потоков делают sharedCounter++ без защиты; ожидали 10 000, получают меньше.
  • Синхронизация — методы BankAccount помечены synchronized; баланс остаётся корректным.
  • Runnable — одна задача, два потока с разными именами.
  • Пул потоковExecutorService переиспользует потоки вместо new Thread на каждую задачу.

Интерактив — жизненный цикл потока (аналог на Python threading).

API и выбор между platform threads и virtual threads — Асинхронность в Java. Память JVM и synchronizedJVM и потоки.


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/MAUITask.Run + Dispatcher / MainThread
Параллельная обработка списка файловParallel.ForEach
Общий счётчик из нескольких потоковlock, Interlocked, ConcurrentDictionary

Подробнее — статья про async и многопоточность в C#. Класс Thread391, Task392.


JavaScript и TypeScript — event loop и worker_threads

В браузере и в Node.js JavaScript-код в одном потоке исполняется по одной инструкции. Пока ждём ответ сервера, event loop выполняет другие колбэки и микрозадачи (Promise).

Компонент загружается. Это пример интеграции — сейчас мы запрашиваем данные из другой системы.
Пример кода загружается. Это пример интеграции — сейчас мы запрашиваем данные из другой системы.

Где какой инструмент

СредаМного сетевых запросовТяжёлые вычисления на CPU
Браузерfetch, Promise.allWeb Worker
Node.jsasync/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-boundCPU-boundСтатья
Pythonasyncio, threadingmultiprocessing29
Javavirtual threads, CompletableFutureExecutorService, ForkJoinPool298
C#async/awaitParallel, Task.Run39
JavaScriptPromise, event loopworker_threads21
TypeScriptте же примитивы + типы Promise<T>те же + типизация worker17
Goгорутины, каналыгорутины на все ядра21
PHPcurl_multi, Swoole, Fibersотдельные воркеры10
KotlinкорутиныDispatchers.Default222
GroovyExecutorService, GParsGPars parallel collections11

Итоги

  1. Определите тип нагрузки — I/O-bound или CPU-bound.
  2. Для I/O используйте async, потоки (где GIL не мешает), горутины или корутины — главное, чтобы один поток не простаивал в ожидании сети.
  3. Для CPU — процессы или пул с числом воркеров около числа ядер; в Python для CPU потоки почти не дают ускорения.
  4. При общей памяти между потоками нужны lock, synchronized, атомики — иначе гонки.
  5. Для изоляции плагинов и фоновых задач — отдельный процесс или очередь сообщений.

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

ТемаСтатья
Сценарии без кода (сайт, десктоп, сервер)Асинхронность простым языком
Event loop, корутины, колбэкиАсинхронное выполнение
Мьютексы, deadlockУправление потоками
MPI, OpenMP, ускорение на кластереПараллельные вычисления
Каталог embed-примеровIT Code Examples