Deep Engineering

MEASUREMENT

bench/parallel/practice.py

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/interview/python/threads-vs-processes
How to run it
python3.11 bench/parallel/basics.py
python3.12 bench/parallel/basics.py
python3.13 bench/parallel/basics.py
python3.14 bench/parallel/basics.py

python3.13 bench/parallel/cost.py
python3.14 bench/parallel/cost.py

python3.11 bench/parallel/practice.py
python3.12 bench/parallel/practice.py
python3.13 bench/parallel/practice.py
python3.14 bench/parallel/practice.py

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

Замеры для урока «Потоки и процессы»

Скрипт Что делает
basics.py наблюдения без времени: общая память у потока и своя у процесса, что можно передать в пул процессов (pickle), среда — ядра, GIL, -X gil=0, способ запуска процессов, — гонка за общий счётчик без блокировки и с Lock, полный файл squares.py с пулом процессов (способ запуска spawn) и есть ли InterpreterPoolExecutor
cost.py время четырёх одинаковых задач последовательно, в пуле из четырёх потоков и из четырёх процессов — для вычислений и для ожидания; цена запуска пула
practice.py источник ответов двух практических задач урока
python3.11 bench/parallel/basics.py
python3.12 bench/parallel/basics.py
python3.13 bench/parallel/basics.py
python3.14 bench/parallel/basics.py

python3.13 bench/parallel/cost.py
python3.14 bench/parallel/cost.py

python3.11 bench/parallel/practice.py
python3.12 bench/parallel/practice.py
python3.13 bench/parallel/practice.py
python3.14 bench/parallel/practice.py

Записи в runs/: без суффикса — CPython 3.13.7, остальные — с суффиксом версии (3.11.15, 3.12.3, 3.14.7). Сборки обычные, с GIL; os.cpu_count() на машине записи — 2, linux/x86_64. Числа cost.py — этой машины и этого прогона, а не правило: на 3.14.7 потоки в вычислениях дали 0,8, процессы — 1,4. Механизм (GIL) объясняет, почему потоки не ускорили вычисления на Python; сколько дадут процессы на другой машине, надо мерить. time.sleep в cost.py — модель ожидания, а не замер сети.

Что получилось

Память: поток дописал 9 в общий список, процесс — 16 в свою копию; в родителе [9].

pickle: лямбда в пуле потоков работает, в пуле процессов — ошибка: 3.11 и 3.12 — AttributeError: Can't pickle local object, 3.13 — AttributeError: Can't get local object, 3.14 — PicklingError: Can't pickle local object.

Среда: sys._is_gil_enabled() есть с 3.13 и возвращает True; -X gil=0 на 3.13 и 3.14 — Disabling the GIL is not supported by this build, на 3.11 и 3.12 опция не проверяется; способ запуска по умолчанию — fork на 3.11–3.13 и forkserver на 3.14.

Гонка: когда поток уступает ход между чтением и записью (time.sleep(0)), без блокировки итог не 4000, с Lock — 4000; обычный += 1 без уступки в записанных прогонах потерь не дал — это наблюдение, а не гарантия.

Полный файл squares.py (раздел 5; функция на уровне модуля, пул в main(), вызов под if __name__ == "__main__", mp_context=get_context("spawn")): файлом отработал на 3.11–3.14; import squares не создал ни одного процесса; тот же код через python -c — AttributeError в дочернем процессе (Can't get attribute 'compute' на 3.11–3.13, module '__main__' has no attribute 'compute' на 3.14); вариант с пулом на уровне модуля со spawn — RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase. на всех четырёх.

InterpreterPoolExecutor (раздел 6): есть на 3.14.7 и выполнил функцию модуля; на 3.11–3.13 его нет.

Без if __name__ == "__main__": на 3.11–3.13 (fork) скрипт отработал, на 3.14 (forkserver) — RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase.

fork при живом потоке: с 3.12 — DeprecationWarning: This process (pid=…) is multi-threaded, use of fork() may lead to deadlocks in the child.; на 3.11 предупреждения нет.

Время (медианы, 3.13.7, секунды на четыре задачи): вычисления — последовательно 0,502, потоки 0,478, процессы 0,262; ожидание — 0,801, 0,201, 0,212. Старт пула: потоки 0,15 мс, процессы 9,09 мс (на 3.14.7 — 18,86 мс).

Script

48 lines
"""Практика урока «Потоки и процессы»: источник ответов двух задач.

Ответ задачи не сочиняется редакцией — он берётся из записи прогона этого
скрипта (`runs/practice.txt`), а сборка сверяет одно с другим
(`scripts/validate-practice.mjs`).

ЧТО ПРОВЕРЯЮТ ЗАДАЧИ. Первая — что поток пишет в общий список, а процесс —
в свою копию. Вторая — сколько займут четыре ожидания по 0,2 секунды в
пуле из четырёх потоков: ожидание GIL не держит, и они идут одновременно.

Код — под `if __name__ == "__main__"`: при запуске процессов способом
spawn или forkserver дочерний процесс заново импортирует этот файл.

Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import multiprocessing
import sys
import threading
import time
from concurrent.futures import ThreadPoolExecutor

results = []


def work(n):
    results.append(n * n)


if __name__ == "__main__":
    print("PY", sys.version.split()[0])
    print()
    print("вывод задачи:")

    t = threading.Thread(target=work, args=(3,))
    t.start()
    t.join()
    p = multiprocessing.Process(target=work, args=(4,))
    p.start()
    p.join()
    print(results)

    print()
    print("оценка:")
    t0 = time.perf_counter()
    with ThreadPoolExecutor(4) as pool:
        list(pool.map(time.sleep, [0.2] * 4))
    print(f"4 × sleep(0.2) в пуле из 4 потоков, секунд: {time.perf_counter() - t0:.1f}")