Потоки и процессы: что выбрать для вычислений, а что — для ожидания
«Потоки или процессы?» — вопрос, на который отвечают правилом, а проверяют пониманием причины. Прогон на 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 в
нём не определены:
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:
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:
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, 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) старт пула и одна пустая задача, мс, медианы 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__ должен быть доступен рабочим процессам для импорта. Поэтому ProcessPoolExecutor не работает в интерактивном интерпретаторе).
А пул на уровне модуля со __main__ module must be importable by worker subprocesses. This means that ProcessPoolExecutor will not work in the interactive interpreterspawn упал с той же RuntimeError на всех
четырёх версиях — не только на 3.14, где её вызвал forkserver по
умолчанию.
Пул процессов на 3.14 в замере стартовал за 18,86 мс. Напрямую с 9,09 на 3.13 это не сравнить: при разных способах запуска пул запускает рабочие процессы по-разному.
Глубже: GIL не защищает ваш код от гонок
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.12 | fork при живых потоках выдаёт 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. |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
t = threading.Thread(target=work, args=(3,)) t.start() t.join() p = multiprocessing.Process(target=work, args=(4,)) p.start() p.join() print(results)
Практика · оцените
Проверьте себя
Сервис скачивает 100 страниц по сети. Что взять?
Чем измерено
Числа и наблюдения этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Поток — это ещё одна линия выполнения в том же процессе, с той же памятью; процесс — отдельная программа со своей памятью и своим интерпретатором. В обычной сборке 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.
На самом деле
- Для ожидания — полезны: в прогоне четыре ожидания по 0,2 с в четырёх потоках заняли 0,201 с вместо 0,801. В сборке с GIL не ускоряются вычисления на Python; расширения на C могут отпускать 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.12
forkпри живых потоках выдаёт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.<
Что разобрано
- База: поток и процесс
- Механизм 1: вычисления и ожидание
- Механизм 2: общая память и pickle
- Механизм 3: цена процессов и способ запуска
- Глубже: GIL не защищает ваш код от гонок
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
6 ИСТОЧНИКОВ
- 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 - 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
- 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 - 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
- Глоссарий 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
- 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