Deep Engineering
Средний·Опубликовано·3.11 · 3.12 · 3.13 · 3.14·25 МИН

Потоки и процессы: что выбрать для вычислений, а что — для ожидания

«Потоки или процессы?» — вопрос, на который отвечают правилом, а проверяют пониманием причины. Прогон на CPython 3.13.7 (обычная сборка с GIL, linux, 2 ядра) отвечает: четыре вычислительные задачи в четырёх потоках заняли 0,478 с против 0,502 последовательно, в четырёх процессах — 0,262; четыре ожидания по 0,2 с в потоках — 0,201 с; поток пишет в общий список, а процесс — в свою копию; лямбду в пул процессов не передать; пул процессов создают в main() и запускают файлом; и на 3.14 под Linux процессы по умолчанию запускаются способом forkserver, а не fork.

Полное техническое изложение

TL;DR

Поток — это ещё одна линия выполнения в том же процессе, с той же памятью; процесс — отдельная программа со своей памятью и своим интерпретатором. В обычной сборке CPython — сборке с GIL — потоки одного процесса выполняют Python-код по очереди: их разделяет глобальная блокировка интерпретатора, GIL. У каждого процесса свой GIL.

Отсюда главное правило для сборки с GIL: для ожидания — потоки, для вычислений на Python — процессы. Пока поток ждёт сеть или диск, GIL свободен: в прогоне четыре ожидания по 0,2 с в четырёх потоках заняли 0,201 с. Пока поток выполняет Python-код, он держит GIL: четыре вычислительные задачи в четырёх потоках — 0,478 с против 0,502 последовательно, в четырёх процессах — 0,262 с. Это CPython 3.13.7 на машине с двумя ядрами; на 3.14.7 отношения вышли другими, и на своей машине их надо мерить. Исключение из правила — расширения на C: некоторые отпускают GIL на тяжёлых вычислениях, например при сжатии или хешировании.

Дальше — то, что отличает понимание от пересказа. Процессы не делят память: поток дописал в общий список, процесс — в свою копию, и родитель этого не увидел. Задачу процессу передают через pickle — сериализацию объекта в байты, — и лямбда так не передаётся. Пул процессов создают внутри main() под if __name__ == "__main__": и запускают файлом. Запуск пула процессов в прогоне стоил миллисекунды, пула потоков — доли миллисекунды. GIL не делает атомарным «прочитать — изменить — записать»: когда поток уступал ход между чтением и записью, обновления общего счётчика терялись. На 3.14 способ запуска процессов по умолчанию на Linux — forkserver вместо fork, сборка без GIL официально поддерживается, а в concurrent.futures появился InterpreterPoolExecutor.

Порог входа
Перед уроком достаточно понимать
  • что такое функция и глобальная переменная;
  • что программа может ждать сеть или диск;
  • зачем в скрипте проверка if __name__ == "__main__": — это урок «Модули и импорт»;
  • что блок with в конце закрывает то, что открыл, — здесь он останавливает пул; подробно — в уроке о контекстных менеджерах, он стоит в линии позже;
  • что lambda — функция без имени, а try / except перехватывает исключение; подробно — в уроках о lambda и об исключениях, они в линии позже;
  • что такое GIL — хотя бы в одну фразу, она есть в выжимке выше; подробно — статья о GIL.
Заранее знать не нужно
  • threading, multiprocessing, concurrent.futures;
  • вычисления против ожидания, цена запуска пула, pickle;
  • гонка и Lock, способы запуска процессов, изменения 3.13 и 3.14.

На чём проверено: CPython 3.11.15, 3.12.3, 3.13.7 и 3.14.7 — обычные сборки с GIL, linux/x86_64, os.cpu_count() = 2; время мерили на 3.13.7 и 3.14.7. Документация сверена 14.10.2026. На эту дату вышел и Python 3.15 (9 октября 2026) — урок на нём не запускался; сборку без GIL урок тоже не измерял.

База: поток и процесс

Фрагмент — как выглядят два пула; fetch, urls, compute и chunks в нём не определены:

PYTHON
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
 
with ThreadPoolExecutor(4) as pool:      # четыре потока в этом процессе
    results = list(pool.map(fetch, urls))
 
with ProcessPoolExecutor(4) as pool:     # четыре отдельных процесса
    results = list(pool.map(compute, chunks))

Полный файл squares.py — тот же пул процессов в виде, который можно скопировать в пустую папку и запустить командой python3 squares.py. Запускать именно файлом — не в интерактивном интерпретаторе (REPL) и не через python -c:

PYTHON
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()

Три правила этого шаблона: функция, которую выполняют процессы, объявлена на уровне модуля; данные и пул создаются внутри main(); main() вызывается под if __name__ == "__main__":. Способ запуска spawn задан явно: так дочерний процесс стартует новым интерпретатором и заново импортирует этот файл — как на Windows и macOS, где spawn — способ по умолчанию. Импорт файла при этом пул не создаёт. Как этот шаблон отработал на четырёх версиях и что бывает без проверки __name__ — в механизме 3.

Ответ уровня Junior на «чем поток отличается от процесса»: потоки работают в одном процессе и делят память, процессы изолированы; в обычной сборке CPython из-за GIL потоки не ускоряют вычисления на Python, поэтому их берут для ввода-вывода, а для вычислений — процессы. Это верно, и за этим ответом идут уточнения: насколько потоки не ускоряют, что значит «делят память» на деле, что можно передать процессу и зачем блокировки, если есть GIL. Механизмы ниже отвечают на них прогоном.

Механизм 1: вычисления и ожидание

наблюдение замераbench/parallel/cost.py, разделы 1 и 2: четыре задачи по 2 000 000 шагов цикла «сумма квадратов» или по time.sleep(0.2); медианы пяти кругов; CPython 3.13.7 с GIL, linux/x86_64, os.cpu_count() = 2; 3.14.7 — в записи cost-3.14.txt. Это наблюдение одной конфигурации: на 3.14.7 отношения другие — 0,8 и 1,4. Механизм (GIL) объясняет, почему потоки не ускорили вычисления; сколько дадут процессы у вас — мерить.

Четыре одинаковые задачи — последовательно, в пуле из четырёх потоков и в пуле из четырёх процессов. Вывод bench/parallel/cost.py, разделы 1 и 2:

1) вычисления: 4 задачи «сумма квадратов», секунды, медианы 5 кругов
   последовательно     0.502   ускорение  1.0
   4 потока            0.478   ускорение  1.1
   4 процесса          0.262   ускорение  1.9

2) ожидание: 4 задачи time.sleep(0.2), секунды, медианы 5 кругов
   последовательно     0.801   ускорение  1.0
   4 потока            0.201   ускорение  4.0
   4 процесса          0.212   ускорение  3.8
  • Вычисления. Потоки почти не помогли: пока поток выполняет Python-код, он держит GIL, и остальные ждут своей очереди. Процессы считали параллельно — у каждого свой интерпретатор и свой GIL: ускорение 1,9 на машине, где os.cpu_count() — 2. На 3.14.7 в той же задаче потоки оказались даже медленнее последовательного запуска (0,8), процессы — 1,4. Эти отношения — одного прогона на одной машине: они зависят от версии, числа ядер и цены запуска процессов.
  • Ожидание. Потоки ускорили вчетверо: time.sleep отпускает GIL, и четыре ожидания идут одновременно. Здесь sleep — модель ожидания: при вводе-выводе GIL тоже отпускается, но настоящую сеть или базу этот замер не проверяет. Процессы дали почти то же, но дороже — об этом механизм 3.

Правило «вычисления — в процессы» относится к Python-коду в сборке с GIL. Расширения на C могут отпускать GIL сами — в глоссарии Python: some extension modules, either standard or third-party, are designed so as to release the GIL when doing computationally intensive tasks such as compression or hashing (некоторые модули расширения, стандартные и сторонние, отпускают GIL на тяжёлых вычислениях — например, при сжатии или хешировании). Такие вычисления потоки ускорить могут; урок это не измерял.

Как именно GIL переключается между потоками и почему его держат, разобрано в статье о GIL.

Механизм 2: общая память и pickle

контракт языкаВывод — прогон bench/parallel/basics.py, разделы 1 и 2. Раздел 1 одинаков на CPython 3.11–3.14; текст ошибки pickle различается по версиям — см. ниже.

Вывод bench/parallel/basics.py, разделы 1 и 2, CPython 3.13.7:

1) память: поток против процесса
   после потока  shared = [9]
   после процесса shared = [9] | exitcode процесса: 0

2) что можно отдать в пул
   потоки,   функция модуля -> 25
   потоки,   lambda         -> 25
   процессы, функция модуля -> 25
   процессы, lambda         -> AttributeError: Can't get local object 'main.<locals>.<lambda>'

Функция дописывает квадрат числа в глобальный список. Поток дописал 9 — и основная программа это видит: память у потоков общая. Процесс дописал 16 в свою копию списка и завершился успешно, но в родителе по-прежнему [9]: у процесса своя память. Результат из процесса возвращают явно — через pool.submit(...).result(), очередь или канал.

Чтобы передать задачу и аргументы в другой процесс, их сериализуют модулем pickle. Функцию модуля он передаёт по имени, а локальную функцию и лямбду — нет: пулу потоков лямбда подошла, пулу процессов — нет. Ошибка зависит от версии: на 3.11–3.13 это AttributeError (текст на 3.11 и 3.12 — Can't pickle local object, на 3.13 — Can't get local object), а на 3.14 — уже другой тип, PicklingError: Can't pickle local object. Код, который ловил except AttributeError, на 3.14 эту ошибку пропустит.

Механизм 3: цена процессов и способ запуска

наблюдение замераВремя — bench/parallel/cost.py, раздел 3, CPython 3.13.7 и 3.14.7, linux/x86_64, медианы пяти кругов. Способ запуска и полный файл squares.py — bench/parallel/basics.py, разделы 3 и 5, CPython 3.11–3.14, и Lib/multiprocessing/context.py.

Вывод bench/parallel/cost.py, раздел 3, CPython 3.13.7:

3) старт пула и одна пустая задача, мс, медианы 5 кругов
   пул потоков        0.15
   пул процессов      9.09

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

Как запускается процесс, зависит от системы и версии. Вывод bench/parallel/basics.py, раздел 3:

3) среда
   sys.platform                    -> linux
   os.cpu_count()                  -> 2
   sys._is_gil_enabled()           -> True
   multiprocessing.get_start_method() -> fork
   python -X gil=0                 -> Fatal Python error: config_read_gil: Disabling the GIL is not supported by this build
   запуск процесса без if __name__ == '__main__' -> exitcode 0
   fork при живом потоке           -> DeprecationWarning: This process (pid=…) is multi-threaded, use of fork() may lead to deadlocks in the child.

Это вывод 3.13. fork копирует память родителя, но из его потоков — только тот, что вызвал fork; если в родителе работают другие потоки, дочерний процесс может зависнуть на блокировке, которую держал несуществующий в нём поток. С 3.12 Python предупреждает об этом — последняя строка; на 3.11 предупреждения нет.

На 3.14 под Linux в строке get_start_method() — forkserver: способ по умолчанию сменили на более безопасный для потоков. spawn запускает дочерний процесс новым интерпретатором, forkserver ответвляет его от заранее запущенного чистого процесса-сервера, и в обоих случаях главный модуль импортируется заново. Поэтому код, запускающий процессы, ставят под if __name__ == "__main__":. На 3.13 скрипт без этой проверки отработал (exitcode 0), а на 3.14 тот же скрипт упал: RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase.

Шаблон squares.py из базы проверен тем же скриптом: файл записывается во временную папку, запускается, импортируется и отдаётся интерпретатору через python -c; для сравнения — тот же пул без проверки __name__, на уровне модуля. Вывод bench/parallel/basics.py, раздел 5, CPython 3.13.7:

5) полный файл squares.py: пул процессов, способ запуска spawn
   python squares.py               -> [285, 328350, 332833500, 333283335000]
   import squares: дочерних процессов -> 0 | stderr пуст: True
   тот же код через python -c      -> AttributeError: Can't get attribute 'compute' on <module '__main__' (<class '_frozen_importlib.BuiltinImporter'>)>
   без if __name__, пул на уровне модуля -> RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase.

Файлом шаблон отработал на всех четырёх версиях, а import squares не создал ни одного процесса: пул появляется только при вызове main(). Через python -c у главного модуля нет файла: дочерний процесс не может его импортировать и не находит compute (текст ошибки зависит от версии, на 3.14 — AttributeError: module '__main__' has no attribute 'compute'). Это ограничение из документации: The __main__ module must be importable by worker subprocesses. This means that ProcessPoolExecutor will not work in the interactive interpreter (модуль __main__ должен быть доступен рабочим процессам для импорта. Поэтому ProcessPoolExecutor не работает в интерактивном интерпретаторе). А пул на уровне модуля со spawn упал с той же RuntimeError на всех четырёх версиях — не только на 3.14, где её вызвал forkserver по умолчанию.

Пул процессов на 3.14 в замере стартовал за 18,86 мс. Напрямую с 9,09 на 3.13 это не сравнить: при разных способах запуска пул запускает рабочие процессы по-разному.

Глубже: GIL не защищает ваш код от гонок

контракт языкаВывод — прогон bench/parallel/basics.py, раздел 4, одинаковый на CPython 3.11–3.14, обычные сборки с GIL. Гонку вызывает уступка хода между чтением и записью — time.sleep(0); строка «просто += 1» — для сравнения, без уступки: отсутствие потерь в ней — наблюдение, а не гарантия.

GIL в обычной сборке гарантирует, что два потока не выполняют байт-код одновременно, но не то, что ваша операция «прочитать, изменить, записать» выполнится без перерыва. Четыре потока по тысяче раз читают общий счётчик, уступают очередь вызовом time.sleep(0) и записывают значение плюс один. Вывод bench/parallel/basics.py, раздел 4:

4) гонка: 4 потока по 1000 увеличений общего счётчика
   без блокировки  итог == 4000: False
   с Lock          итог == 4000: True
   просто += 1     итог == 4000: True

Без блокировки два потока успевают прочитать одно и то же значение, и одно увеличение теряется. С threading.Lock чтение и запись выполняются одним куском, и итог точный. Последняя строка — обычный += 1 без уступки: в записанных прогонах итог совпал с 4000. Это наблюдение, а не гарантия: отсутствие потерь в одном запуске не доказывает, что их не будет в другом — в другой версии, сборке или под другой нагрузкой. Язык атомарность такой операции не обещает: FAQ Python относит D[x] = D[x] + 1 к неатомарным операциям и заканчивает советом When in doubt, use a mutex! (Сомневаетесь — берите мьютекс!). Правило: общее изменяемое состояние между потоками защищают блокировкой или не делят вовсе — передают данные через queue.Queue.

sys._is_gil_enabled() появилась в 3.13 — вместе с возможностью собрать CPython без GIL. В обычной сборке, на которой сделаны все замеры урока, она возвращает True, а попытка выключить GIL опцией -X gil=0 заканчивается ошибкой Disabling the GIL is not supported by this build — строка -X gil=0 раздела «среда» выше. На 3.11 и 3.12 этой опции ещё нет, и интерпретатор её просто не проверяет.

В 3.14 у сборки без GIL новый статус — в What's New 3.14: PEP 779: Free-threaded Python is officially supported (PEP 779: Python без GIL официально поддерживается). Это по-прежнему отдельная сборка: обычный интерпретатор 3.14.7 в прогоне — с GIL. В сборке без GIL потоки могут выполнять Python-код параллельно, поэтому правило «вычисления — в процессы» к ней напрямую не относится; урок её не измерял.

Ещё один вариант из 3.14 — concurrent.futures.InterpreterPoolExecutor: пул из нескольких изолированных интерпретаторов в одном процессе. По документации, Each interpreter has its own Global Interpreter Lock, which means code run with this executor has true multi-core parallelism (у каждого интерпретатора свой GIL, поэтому код в этом пуле выполняется по-настоящему параллельно на нескольких ядрах); изменяемые объекты интерпретаторы между собой не делят. Вывод bench/parallel/basics.py, раздел 6, CPython 3.14.7:

6) пул интерпретаторов в одном процессе
   concurrent.futures.InterpreterPoolExecutor -> есть, функция модуля -> 25

На 3.11–3.13 этого класса нет. Его время урок не мерил.

Как отвечать на собеседовании

Короткий ответ: потоки делят память процесса, процессы изолированы. В обычном CPython — сборке с GIL — потоки выполняют Python-код по очереди, поэтому для ожидания — сети, диска — берут потоки или asyncio, а для вычислений на Python — процессы; расширения на C могут отпускать GIL сами. Процессам задачи передают через pickle, запуск процессов дороже, а пул процессов создают в main() под if __name__ == "__main__":. GIL не избавляет от гонок: общее состояние защищают Lock.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

На «насколько потоки не ускоряют вычисления». «В моём замере на CPython 3.13.7, на машине с двумя ядрами, четыре задачи в четырёх потоках — 0,478 с против 0,502 последовательно, в процессах — 0,262. А четыре ожидания по 0,2 с в потоках заняли 0,201. На 3.14.7 отношения были другие, так что для своей задачи я бы мерил».

На «зачем Lock, если есть GIL». «GIL не делает атомарной мою операцию "прочитать — изменить — записать". Когда поток уступает ход между чтением и записью, четыре потока по тысяче увеличений не дали 4000, с Lock — дали. Обычный += в моих прогонах потерь не дал, но это не гарантия: в FAQ Python D[x] = D[x] + 1 — среди неатомарных операций».

На «почему лямбда не работает в ProcessPoolExecutor». «Задачу передают в другой процесс через pickle, а он передаёт функцию по имени, по которому её можно найти импортом. У лямбды и локальной функции такого имени нет. В пуле потоков лямбда работает».

На «что изменилось в последних версиях». «В 3.13 появилась сборка без GIL и sys._is_gil_enabled(); в 3.14 сборка без GIL получила статус официально поддерживаемой, но обычная сборка по-прежнему с GIL. В 3.14 же способ запуска процессов по умолчанию на Linux — forkserver вместо fork, и появился InterpreterPoolExecutor — пул интерпретаторов в одном процессе».

Дальше спросят

Спросят дальше

Чем потоки отличаются от asyncio?

Короткий ответ

Потоки переключаются без участия вашего кода — между любыми операциями; в asyncio задачи переключаются только в точках await, в одном потоке. Для тысяч одновременных ожиданий в asyncio не нужен поток на каждое. Для кода, который не умеет await, — например, синхронного драйвера базы, — остаются потоки. Подробно — в уроке об async/await.

Спросят дальше

Как вернуть результат из процесса?

Короткий ответ

Не через глобальную переменную — в прогоне процесс дописал в свою копию списка, а родитель её не увидел. Через ProcessPoolExecutor: submit возвращает future, result() — значение, переданное обратно тем же pickle. Или через multiprocessing.Queue и Pipe.

Спросят дальше

Что такое сборка без GIL?

Короткий ответ

Отдельная сборка CPython, появившаяся в 3.13, в которой потоки могут выполнять Python-код параллельно; в 3.14 она получила статус официально поддерживаемой. Её нужно устанавливать отдельно: обычная сборка — с GIL, sys._is_gil_enabled() в ней возвращает True, а -X gil=0 — ошибку, как в прогоне урока. Всё сказанное в уроке о потоках — про обычную сборку.

Частые заблуждения

Утверждение

Потоки в Python бесполезны из-за GIL

На самом деле

Для ожидания — полезны: в прогоне четыре ожидания по 0,2 с в четырёх потоках заняли 0,201 с вместо 0,801. В сборке с GIL не ускоряются вычисления на Python; расширения на C могут отпускать GIL и на вычислениях.

Утверждение

Раз есть GIL, блокировки в потоках не нужны

На самом деле

GIL не делает атомарной операцию «прочитать — изменить — записать». Когда поток уступает ход между шагами, четыре потока по тысяче увеличений без Lock не дали 4000. А то, что обычный += в прогоне потерь не дал, — наблюдение, а не гарантия.

Утверждение

Процесс может изменить переменную родителя

На самом деле

У процесса своя память: процесс дописал в список, а в родителе осталось [9]. Результат возвращают явно — через future, очередь или канал.

Утверждение

Процессы всегда быстрее потоков

На самом деле

Запуск пула процессов в замере — 9,09 мс против 0,15 у потоков, и каждую задачу надо сериализовать. На мелких задачах и на ожидании процессы проигрывают, а ускорение вычислений зависит от машины: на двух ядрах — 1,9 на 3.13.7 и 1,4 на 3.14.7.

История версий

ВерсияИзменениеЧто это значит для кода
3.11Опорная точка урока. Ошибка при передаче лямбды в пул процессов — AttributeError: Can't pickle local object.
3.12fork при живых потоках выдаёт DeprecationWarning: дочерний процесс может зависнуть.
3.13Появились сборка без GIL и sys._is_gil_enabled(); в обычной сборке функция возвращает True, а -X gil=0 — ошибку. Ошибка pickle для лямбды — AttributeError: Can't get local object.
3.14Способ запуска процессов по умолчанию на Linux — forkserver вместо fork: главный модуль импортируется заново, и скрипт без if __name__ == "__main__" в прогоне упал с RuntimeError. Ошибка pickle для лямбды — другой тип: PicklingError. Сборка без GIL официально поддерживается (PEP 779) — урок её не измерял; появился concurrent.futures.InterpreterPoolExecutor.
3.15Вышел 9 октября 2026. Урок на нём не запускался: прогоны — CPython 3.11–3.14, сверено 14.10.2026.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Функция work дописывает квадрат числа в глобальный список results. Её запускают в потоке, потом в процессе. Что напечатает этот код? Это фрагмент: импорты и проверка __name__ опущены, полный файл — bench/parallel/practice.py.
t = threading.Thread(target=work, args=(3,))
t.start()
t.join()
p = multiprocessing.Process(target=work, args=(4,))
p.start()
p.join()
print(results)

Практика · оцените

Четыре вызова time.sleep(0.2) отдали пулу из четырёх потоков. Сколько секунд займёт всё вместе?
с

Проверьте себя

Вопрос 1 из 4

Сервис скачивает 100 страниц по сети. Что взять?

Чем измерено

Числа и наблюдения этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.

Источники и что читать дальше

6 ИСТОЧНИКОВ

  1. Lib/multiprocessing/context.py — способ запуска процессов по умолчаниюИсходный код CPython. На теге v3.13.7, строки 323–328: на macOS — spawn, на остальных POSIX-системах — _default_context = DefaultContext(_concrete_contexts['fork']). На теге v3.14.0, строки 323–328: «gh-84559: We changed everyones default to a thread safeish one in 3.14» (gh-84559: в 3.14 мы сменили всем способ по умолчанию на более безопасный для потоков), и там, где система умеет передавать дескрипторы и это не macOS, — DefaultContext(_concrete_contexts['forkserver']).https://github.com/python/cpython/blob/v3.14.0/Lib/multiprocessing/context.py
  2. sys._is_gil_enabledОфициальная документация. Строка документации функции, появившейся в 3.13: «Return True if the GIL is currently enabled and False otherwise» (Вернуть True, если GIL сейчас включён, и False в противном случае). Сверено вызовом sys._is_gil_enabled.__doc__ на 3.13.7; на 3.11 и 3.12 функции нет.https://docs.python.org/3.13/library/sys.html#sys._is_gil_enabled
  3. What's New In Python 3.14 — сборка без GIL, InterpreterPoolExecutor, способ запускаОфициальная документация. В главном списке 3.14: «PEP 779: Free-threaded Python is officially supported» (PEP 779: Python без GIL официально поддерживается). О новом пуле: «Add a new executor class, InterpreterPoolExecutor, which exposes multiple Python interpreters in the same process ('subinterpreters') to Python code. This uses a pool of independent Python interpreters to execute calls asynchronously» (Добавлен новый класс пула, InterpreterPoolExecutor, который даёт Python-коду несколько интерпретаторов в одном процессе („субинтерпретаторы“). Вызовы выполняются асинхронно в пуле независимых интерпретаторов). О способе запуска: «This change does not affect Windows or macOS, where 'spawn' remains the default start method» (Это изменение не касается Windows и macOS, где способом запуска по умолчанию остаётся spawn). Сборку без GIL урок не измерял: все прогоны — обычные сборки с GIL. Наличие InterpreterPoolExecutor проверено прогоном bench/parallel/basics.py, раздел 6: есть на 3.14.7, нет на 3.11–3.13. Сверено 14.10.2026.https://docs.python.org/3/whatsnew/3.14.html
  4. concurrent.futures — пул процессов и пул интерпретаторовОфициальная документация. О ProcessPoolExecutor: «The __main__ module must be importable by worker subprocesses. This means that ProcessPoolExecutor will not work in the interactive interpreter» (Модуль __main__ должен быть доступен рабочим процессам для импорта. Поэтому ProcessPoolExecutor не работает в интерактивном интерпретаторе). Об InterpreterPoolExecutor, добавленном в 3.14: «Each interpreter has its own Global Interpreter Lock, which means code run with this executor has true multi-core parallelism» (У каждого интерпретатора свой GIL, поэтому код в этом пуле выполняется по-настоящему параллельно на нескольких ядрах) и «The worker interpreters are isolated from each other, which means each has its own runtime state and that they can't share any mutable objects or other data» (Рабочие интерпретаторы изолированы друг от друга: у каждого своё состояние, и изменяемые объекты и другие данные они делить не могут). Сверено 14.10.2026.https://docs.python.org/3/library/concurrent.futures.html
  5. Глоссарий Python: global interpreter lockОфициальная документация. «However, some extension modules, either standard or third-party, are designed so as to release the GIL when doing computationally intensive tasks such as compression or hashing. Also, the GIL is always released when doing I/O» (Однако некоторые модули расширения, стандартные и сторонние, отпускают GIL на тяжёлых вычислениях — например, при сжатии или хешировании. А при вводе-выводе GIL отпускается всегда). Сверено по документации 3.14 14.10.2026; расширения урок не измерял.https://docs.python.org/3.14/glossary.html#term-global-interpreter-lock
  6. Library and Extension FAQ — какие изменения общих значений потокобезопасныОфициальная документация. В списке операций, которые не атомарны, — D[x] = D[x] + 1: такое же «прочитать — изменить — записать», как state["n"] += 1 в разделе 4 прогона. Ответ FAQ заканчивается советом «When in doubt, use a mutex!» (Сомневаетесь — берите мьютекс!). Сверено 14.10.2026.https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe