В чём опасность запуска дочернего процесса через fork после создания потоков?
После fork дочерний процесс наследует состояние памяти процесса, но в нём остаётся только поток, вызвавший fork. Если другой поток удерживал блокировку или библиотека имела внутреннее состояние, связанное с потоками, дочерний процесс может навсегда зависнуть или работать некорректно. Для такого сценария обычно безопаснее использовать spawn или forkserver.
fork — механизм Unix, который создаёт процесс как копию текущего адресного пространства. Он эффективен благодаря копированию страниц памяти по принципу copy-on-write: физически страницы копируются только при изменении.
Python предоставляет разные способы запуска дочерних процессов, потому что у копирования процесса и запуска нового интерпретатора разные свойства. spawn создаёт новый интерпретатор с чистым состоянием, а fork быстрее стартует, но наследует состояние родителя, включая потенциально опасное состояние потоковой инфраструктуры.
Предположим, один поток держит блокировку внутри файлового, сетевого или стороннего библиотечного кода, а другой поток создаёт дочерний процесс через fork. В дочернем процессе останется заблокированная структура, но поток, который мог бы её освободить, уже не существует.
Попытка захватить такую блокировку в дочернем процессе может привести к взаимной блокировке. Даже если явной блокировки нет, дочерний процесс может унаследовать незавершённое состояние буферов, соединений, генераторов случайных чисел или рантайма, рассчитанное на наличие исходных потоков.
Ключевой факт: fork копирует память, но не все потоки. После него дочерний процесс продолжает выполнение только из потока, вызвавшего fork; остальные потоки не переносятся в дочерний процесс.
Состояние обычной блокировки при этом копируется буквально. Если в родителе она была захвачена исчезнувшим потоком, в дочернем процессе она может остаться захваченной навсегда. Это отличается от межпроцессной синхронизации: речь идёт о скопированном состоянии памяти, а не о новой корректно настроенной блокировке.
На практике после fork также опасно повторно использовать унаследованные сетевые соединения, файловые буферы и объекты, созданные многопоточной библиотекой. Их внутреннее состояние могло быть изменено другим потоком в момент копирования.
spawn решает проблему иначе: он запускает новый интерпретатор Python и импортирует необходимый модуль, не копируя рабочее состояние родительских потоков. Цена этого решения — более медленный запуск и необходимость сделать целевую функцию и передаваемые аргументы доступными для сериализации; код создания процессов должен быть защищён условием запуска главного модуля.
forkserver позволяет отделить создание процессов от многопоточного приложения: специальный сервер запускается в контролируемом состоянии и порождает дочерние процессы. Это может быть компромиссом между безопасностью и стоимостью запуска, но доступность и поведение вариантов зависят от платформы и версии Python.
Здесь новый процесс не получает копию уже работающих потоков и их блокировок. Однако spawn не делает произвольные объекты автоматически передаваемыми: целевая функция и аргументы должны соответствовать требованиям сериализации.
В асинхронном веб-сервисе библиотека HTTP-клиента использует фоновые потоки, а приложение пытается запускать вычислительные процессы через fork. Иногда дочерний процесс зависает при первом сетевом запросе: он унаследовал внутреннюю блокировку или файловый дескриптор клиента, созданного до fork.
Вариант оставить fork быстрым и просто повторить попытку плох: зависание будет редким и трудно воспроизводимым, а повторное использование соединений после fork остаётся некорректным. Вариант полностью отказаться от процессов и использовать потоки проще, но для CPU-bound работы в CPython он обычно не даёт настоящего параллельного выполнения Python-кода из-за GIL.
Практичное решение — выбрать spawn или forkserver, создавать клиентские соединения уже внутри дочернего процесса и явно передавать только необходимые данные. Это увеличивает стоимость запуска и требует аккуратной структуры модулей, зато устраняет наследование живого потокового состояния; в результате сервис получает предсказуемое поведение вместо редких зависаний.
fork безопасным, если в момент вызова все пользовательские потоки простаивают?Нет. Отсутствие активной пользовательской работы не доказывает, что внутренние блокировки и структуры библиотек находятся в безопасном состоянии. Кроме того, между проверкой состояния и вызовом fork другой поток может начать операцию. Безопасность возможна только при строгом контроле жизненного цикла всех библиотек и потоков, что на практике трудно гарантировать.
fork нельзя безоговорочно использовать унаследованное сетевое соединение?Дескриптор соединения копируется в дочерний процесс и может ссылаться на тот же ресурс операционной системы. Но буферы, протоколы, фоновые потоки и состояние объекта-клиента копируются независимо и могут быть несогласованы. Надёжный подход — закрыть унаследованный клиент и создать новое соединение уже в дочернем процессе.
spawn все проблемы multiprocessing?Нет. Он устраняет именно наследование состояния родительского процесса и его потоков, но добавляет сериализацию целевой функции и аргументов. Нельзя полагаться на произвольные локальные функции, открытые дескрипторы или объекты, не поддерживающие передачу между процессами. Кроме того, ошибки проектирования IPC, утечки ресурсов и неправильное завершение процессов остаются возможными.