ЗАМЕР
bench/parallel/basics.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 мс).
Скрипт
272 строк"""Потоки и процессы: что общее, что нет и что из этого следует.
Времени здесь нет — только наблюдения. Цена — в cost.py.
ШЕСТЬ СЮЖЕТОВ:
1. ПАМЯТЬ. Поток меняет тот же список, что и основная программа; процесс —
свою копию, и родитель изменений не видит.
2. ЧТО МОЖНО ОТДАТЬ ПРОЦЕССУ. Задачу и аргументы процессу передают через
pickle, поэтому лямбда или локальная функция в ProcessPoolExecutor не
годится; потоку — годится что угодно.
3. СРЕДА. Число ядер, включён ли GIL (sys._is_gil_enabled есть с 3.13),
можно ли его выключить в этой сборке (-X gil=0) и каким способом
multiprocessing запускает процессы по умолчанию.
4. ГОНКА. Чтение, изменение и запись общего счётчика из нескольких потоков,
когда поток уступает ход между чтением и записью (time.sleep(0)), теряют
обновления; с Lock — нет. Для сравнения — обычный `+= 1` без уступки: в
записанных прогонах потерь не видно, но это не гарантия.
5. ПОЛНЫЙ ФАЙЛ С ПУЛОМ ПРОЦЕССОВ. Шаблон из урока (строка POOL_EXAMPLE):
функция на уровне модуля, пул внутри main(), вызов под
`if __name__ == "__main__"`, способ запуска явно spawn. Его запускают
файлом; импортируют, чтобы убедиться, что import пул не создаёт; тот же
код отдают через python -c — как в интерактивном интерпретаторе, где у
главного модуля нет файла; и для сравнения — вариант без проверки
__name__, с пулом на уровне модуля (в нём один рабочий процесс, чтобы
тексты ошибок дочерних процессов не перемешивались).
6. INTERPRETERPOOLEXECUTOR. Есть ли в concurrent.futures пул отдельных
интерпретаторов в одном процессе (появился в 3.14) и выполняет ли он
функцию модуля.
Весь код — под `if __name__ == "__main__"`: при запуске процессов способом
spawn или forkserver дочерний процесс заново импортирует этот файл.
Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import multiprocessing
import os
import re
import subprocess
import sys
import threading
import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
shared = []
def append_square(n):
shared.append(n * n)
def square(n):
return n * n
NO_GUARD = """
import multiprocessing
def f():
pass
p = multiprocessing.Process(target=f)
p.start()
p.join()
print("exitcode", p.exitcode)
"""
FORK_WITH_THREAD = """
import multiprocessing, threading, time, warnings
warnings.simplefilter("always")
def f():
pass
if __name__ == "__main__":
threading.Thread(target=time.sleep, args=(1,), daemon=True).start()
with warnings.catch_warnings(record=True) as caught:
p = multiprocessing.get_context("fork").Process(target=f)
p.start()
p.join()
print(caught[0].category.__name__ + ": " + str(caught[0].message) if caught else "предупреждений нет")
"""
# Полный файл из урока: его пишут во временную папку как squares.py.
POOL_EXAMPLE = """\
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import get_context
def compute(n):
return sum(i * i for i in range(n))
def main():
chunks = [10, 100, 1000, 10000]
with ProcessPoolExecutor(4, mp_context=get_context("spawn")) as pool:
print(list(pool.map(compute, chunks)))
if __name__ == "__main__":
main()
"""
POOL_NO_GUARD = """\
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import get_context
def compute(n):
return sum(i * i for i in range(n))
# Один рабочий процесс: иначе тексты ошибок нескольких дочерних процессов
# перемешиваются в stderr построчно.
with ProcessPoolExecutor(1, mp_context=get_context("spawn")) as pool:
print(list(pool.map(compute, [10, 100, 1000, 10000])))
"""
def first_line(text, prefix):
for line in text.splitlines():
if line.startswith(prefix):
return line
return None
def pool_example_cases():
"""Раздел 5: файл squares.py запускают, импортируют и отдают через -c."""
import tempfile
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "squares.py")
with open(path, "w", encoding="utf-8") as f:
f.write(POOL_EXAMPLE)
run = lambda *args: subprocess.run([sys.executable, *args], capture_output=True, text=True,
timeout=120, cwd=tmp)
r = run("squares.py")
print(" python squares.py ->", r.stdout.strip() or r.stderr.strip().splitlines()[-1])
r = run("-c", "import multiprocessing, squares; print(len(multiprocessing.active_children()))")
print(" import squares: дочерних процессов ->", r.stdout.strip(), "| stderr пуст:", not r.stderr.strip())
r = run("-c", POOL_EXAMPLE)
out = r.stdout + r.stderr
print(" тот же код через python -c ->", first_line(out, "AttributeError:") or out.strip().splitlines()[-1])
print(" без if __name__, пул на уровне модуля ->", run_snippet(POOL_NO_GUARD))
def run_snippet(code):
"""Запустить код отдельным файлом и вернуть итог: последнюю строку вывода
или первую строку RuntimeError, если она есть."""
import tempfile
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "snippet.py")
with open(path, "w", encoding="utf-8") as f:
f.write(code)
r = subprocess.run([sys.executable, path], capture_output=True, text=True, timeout=120)
lines = (r.stdout + r.stderr).splitlines()
for i, line in enumerate(lines):
if line.startswith("RuntimeError:"):
# Текст ошибки идёт следующими строками до пустой — собираем первое предложение.
words = [line.strip()]
for nxt in lines[i + 1:]:
if not nxt.strip():
break
words.append(nxt.strip())
text = " ".join(words).replace("RuntimeError: ", "RuntimeError: ", 1)
return text.split(".")[0] + "."
last = lines[-1] if lines else ""
return re.sub(r"pid=\d+", "pid=…", last)
def main():
print("PY", sys.version.split()[0])
print()
# ── 1 ─────────────────────────────────────────────────────────────────
print("1) память: поток против процесса")
t = threading.Thread(target=append_square, args=(3,))
t.start()
t.join()
print(" после потока shared =", shared)
p = multiprocessing.Process(target=append_square, args=(4,))
p.start()
p.join()
print(" после процесса shared =", shared, "| exitcode процесса:", p.exitcode)
print()
# ── 2 ─────────────────────────────────────────────────────────────────
print("2) что можно отдать в пул")
with ThreadPoolExecutor(2) as pool:
print(" потоки, функция модуля ->", pool.submit(square, 5).result())
print(" потоки, lambda ->", pool.submit(lambda n: n * n, 5).result())
with ProcessPoolExecutor(2) as pool:
print(" процессы, функция модуля ->", pool.submit(square, 5).result())
try:
pool.submit(lambda n: n * n, 5).result()
print(" процессы, lambda -> сработало")
except Exception as e: # noqa: BLE001 — нам нужен текст
# Адрес объекта в тексте ошибки меняется от запуска к запуску.
text = re.sub(r"0x[0-9a-f]+", "0x…", f"{type(e).__name__}: {e}")
print(" процессы, lambda ->", text)
print()
# ── 3 ─────────────────────────────────────────────────────────────────
print("3) среда")
print(" sys.platform ->", sys.platform)
print(" os.cpu_count() ->", os.cpu_count())
gil = getattr(sys, "_is_gil_enabled", None)
print(" sys._is_gil_enabled() ->", gil() if gil else "нет такой функции")
print(" multiprocessing.get_start_method() ->", multiprocessing.get_start_method())
r = subprocess.run([sys.executable, "-X", "gil=0", "-c", "pass"], capture_output=True, text=True)
answer = r.stderr.strip().splitlines()[0] if r.returncode else "запустился, опция не проверяется"
print(" python -X gil=0 ->", answer)
print(" запуск процесса без if __name__ == '__main__' ->", run_snippet(NO_GUARD))
print(" fork при живом потоке ->", run_snippet(FORK_WITH_THREAD))
print()
# ── 4 ─────────────────────────────────────────────────────────────────
print("4) гонка: 4 потока по 1000 увеличений общего счётчика")
state = {"n": 0}
lock = threading.Lock()
def unsafe():
for _ in range(1000):
value = state["n"]
time.sleep(0) # уступить: другой поток успеет прочитать то же значение
state["n"] = value + 1
def safe():
for _ in range(1000):
with lock:
value = state["n"]
time.sleep(0)
state["n"] = value + 1
def plain():
for _ in range(1000):
state["n"] += 1 # без уступки между чтением и записью
for name, fn in [("без блокировки", unsafe), ("с Lock", safe), ("просто += 1", plain)]:
state["n"] = 0
threads = [threading.Thread(target=fn) for _ in range(4)]
for th in threads:
th.start()
for th in threads:
th.join()
print(f" {name:<15} итог == 4000: {state['n'] == 4000}")
print()
# ── 5 ─────────────────────────────────────────────────────────────────
print("5) полный файл squares.py: пул процессов, способ запуска spawn")
pool_example_cases()
print()
# ── 6 ─────────────────────────────────────────────────────────────────
print("6) пул интерпретаторов в одном процессе")
import concurrent.futures as cf
ipe = getattr(cf, "InterpreterPoolExecutor", None)
if ipe is None:
print(" concurrent.futures.InterpreterPoolExecutor -> нет")
else:
with ipe(2) as pool:
print(" concurrent.futures.InterpreterPoolExecutor -> есть, функция модуля ->",
pool.submit(square, 5).result())
if __name__ == "__main__":
main()