ЗАМЕР
bench/parallel/practice.py
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/python/threads-vs-processes
- Как запустить
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
Запись прогона
Замеры для урока «Потоки и процессы»
| Скрипт | Что делает |
|---|---|
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 мс).
Скрипт
48 строк"""Практика урока «Потоки и процессы»: источник ответов двух задач.
Ответ задачи не сочиняется редакцией — он берётся из записи прогона этого
скрипта (`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}")