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

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

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

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

Теория процессов и потоков — Процессы и потоки. 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-запрос — логика выбора инструмента та же.

Play ITЗагрузка интерактивного демо…


Python — четыре режима в одной программе​

Полный листинг с прогресс-баром (tqdm), блоками I/O и CPU и ASCII-графиком — в code.spirzen.ru:

Код ITЗагрузка примера кода…

Что сравнивает программа​

Режим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. Основы asyncio — 291.


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

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

Код ITЗагрузка примера кода…

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

  • Простые потоки — 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.

Код ITЗагрузка примера кода…

СитуацияПодходящий API
HTTP, БД, файлыasync/await, Task.WhenAll
Долгий расчёт в окне WPF/MAUITask.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).

Play ITЗагрузка интерактивного демо…

Код ITЗагрузка примера кода…

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

СредаМного сетевых запросовТяжёлые вычисления на 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 ждёт завершения всех "загрузок".

Код ITЗагрузка примера кода…

Планировщик Go (модель G–M–P) может держать сотни тысяч горутин на небольшом числе потоков ядра. Каналы и правило CSP — Асинхронность и горутины. Гонки — Механика языка.


PHP — запрос за запросом и параллельный cURL​

Классическая схема PHP-FPM

  • один входящий HTTP-запрос;
  • один процесс (или воркер);
  • код в скрипте идёт сверху вниз.

Если внутри скрипта пять раз вызвать file_get_contents в цикле, задержки суммируются. Параллельные исходящие HTTP в одном скрипте — через curl_multi_*.

С PHP 8.1 есть встроенные Fibers. Для долгоживущих воркеров и высокой нагрузки — Swoole, RoadRunner, ReactPHP — обзор в экосистеме PHP.

Код ITЗагрузка примера кода…

Практикум в разделе PHP — Параллельные HTTP-запросы. Модель "запрос — ответ" — Что такое PHP.


Kotlin — корутины и async / awaitAll​

Корутина — лёгкая задача на JVM: тысячи корутин дешевле тысячи потоков ОС. suspend-функция при delay или HTTP отдаёт поток другим корутинам.

Пачка "загрузок" — async внутри coroutineScope и awaitAll():

Код ITЗагрузка примера кода…

Корутины в Kotlin. Сравнение с Java — 298, Java и Kotlin.


Groovy на JVM — потоки Java и GPars​

Groovy компилируется в байт-код JVM и использует те же java.util.concurrent API, что и Java. Библиотека GPars добавляет параллельные коллекции и Fork/Join-подобные конструкции.

Код ITЗагрузка примера кода…

Практикум на 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