Twisted: совместное использование нескольких потоков и процессов

#multithreading #unix #twisted #process #reactor

#многопоточность #unix #twisted #процесс #реактор

Вопрос:

Документация Twisted навела меня на мысль, что можно комбинировать такие методы, как reactor.spawnProcess() и threads.deferToThread() в одном приложении, что реактор элегантно справится с этим незаметно. Попробовав это на самом деле, я обнаружил, что мое приложение заходит в тупик. При использовании нескольких потоков самостоятельно или дочерних процессов самостоятельно все в порядке.

Заглядывая в исходный код reactor, я обнаруживаю, что SelectReactor.spawnProcess() метод просто вызывает os.fork() без какого-либо учета нескольких потоков, которые могут быть запущены. Это объясняет взаимоблокировки, потому что, начиная с вызова os.fork() , у вас будут запущены два процесса с несколькими параллельными потоками, которые делают неизвестно что с одними и теми же файловыми дескрипторами.

Мой вопрос к SO заключается в том, какова наилучшая стратегия для решения этой проблемы?

Что я имею в виду, так это создать подкласс SelectReactor , чтобы он был одноэлементным и вызывался os.fork() только один раз, сразу при создании экземпляра. Дочерний процесс будет выполняться в фоновом режиме и выступать в качестве сервера для родительского (используя сериализацию объектов по каналам для обмена данными туда и обратно). Родительский сервер продолжает запускать приложение и может использовать потоки по желанию. Вызовы spawnProcess() в родительском процессе будут делегированы дочернему процессу, который гарантированно будет иметь только один запущенный поток и, следовательно, сможет вызывать os.fork() безопасно.

Кто-нибудь делал это раньше? Есть ли более быстрый способ?

Ответ №1:

Какова наилучшая стратегия для решения этой проблемы?

Отправьте запрос (возможно, после регистрации) с описанием проблемы, предпочтительно с воспроизводимым тестовым примером (для максимальной точности). Затем может состояться некоторое обсуждение того, каким может быть наилучший способ (или способы — разные платформы могут требовать разного решения) для его реализации.

Идея немедленного создания дочернего процесса, который поможет в дальнейшем создании дочернего процесса, поднималась ранее, чтобы решить проблему производительности, связанную с получением результатов дочернего процесса. Если этот подход теперь решает две проблемы, он начинает выглядеть немного привлекательнее. Одна из потенциальных трудностей с этим подходом заключается в том, что spawnProcess синхронно возвращает объект, который предоставляет PID дочернего элемента и позволяет отправлять на него сигналы. Это требует немного больше работы для реализации, если на пути есть промежуточный процесс, поскольку PID необходимо будет передать обратно основному процессу перед spawnProcess возвратом. Аналогичной проблемой будет поддержка childFDs аргумента, поскольку больше не будет возможности просто наследовать файловые дескрипторы в дочернем процессе.

Альтернативным решением (которое может быть несколько более хакерским, но которое также может иметь меньше проблем с реализацией) может заключаться в вызове sys.setcheckinterval с очень большим числом перед вызовом os.fork , а затем восстановить исходный интервал проверки только в родительском процессе. Этого должно быть достаточно, чтобы избежать любого переключения потоков в процессе до тех пор, пока не произойдет os.execvpe уничтожение всех дополнительных потоков. Это не совсем правильно, поскольку это приведет к тому, что определенные ресурсы (такие как мьютексы и условия) будут находиться в нерабочем состоянии, но вы используете их с deferToThread не очень часто, поэтому, возможно, это не повлияет на ваш случай.

Комментарии:

1. Спасибо. Я не думал о дочерних файлах. Вероятно, было бы достаточно некоторой акробатики с dup2, но это означает больше работы. Я сведу это к самому маленькому тестовому варианту, какой только смогу, и задействую twistedmatrix, если смогу это реализовать. Я передам стратегию setcheckinterval, но она может быть приемлемой для других.

Ответ №2:

Совет, который Жан-Поль дает в своем ответе, хорош, но это должно сработать (и работает в большинстве случаев).

Во-первых, Twisted также использует потоки для разрешения имен хостов, и я определенно использовал подпроцессы в процессах Twisted, которые также устанавливают клиентские соединения. Так что это может сработать на практике.

Во-вторых, fork() не создает несколько потоков в дочернем процессе. В соответствии со стандартом, описывающим fork() ,

Процесс должен быть создан с помощью одного потока. Если многопоточный процесс вызывает fork(), новый процесс должен содержать точную копию вызывающего потока …

Это не означает, что нет потенциальных проблем с многопоточностью spawnProcess ; стандарт также гласит:

… чтобы избежать ошибок, дочерний процесс может выполнять операции, безопасные для асинхронного сигнала, только до тех пор, пока не будет вызвана одна из функций exec …

и я не думаю, что есть что-либо, гарантирующее использование только операций, безопасных для асинхронного сигнала.

Итак, пожалуйста, будьте более конкретны в отношении вашей конкретной проблемы, поскольку это не подпроцесс с клонируемыми потоками.

Комментарии:

1. Спасибо за ссылку; Я не знал, что был скопирован только поток, вызывающий fork(). Однако я не уверен, что то, что я делаю, «должно» работать по двум причинам. Во-первых, я слышал, что GNU / Linux не на 100% соответствует Pthreads, поэтому некоторые семантики в стандарте могут быть не реализованы для меня. Во-вторых, я не знаю, что интерпретатор Python может делать под прикрытием, что может привести к моей тупиковой ситуации, когда в дочернем процессе сохраняется только один поток. Мне нужно свести это к наименьшему тестовому варианту, который я могу.

2. fork() создание только одного потока — довольно важная часть pthreads, я почти уверен, что Linux соответствует этой части SUS. Но вы абсолютно правы: трудно сказать, что происходит, без четкого небольшого тестового примера, демонстрирующего проблему.

Ответ №3:

Возвращаясь к этой проблеме через некоторое время, я обнаружил, что если я сделаю это:

reactor.callFromThread(reactor.spawnProcess, *spawnargs)

вместо этого:

reactor.spawnProcess(*spawnargs)

затем проблема исчезает в моем небольшом тестовом примере. В документации Twisted есть замечание «Использование процессов», которое побудило меня попробовать следующее: «Большая часть кода в Twisted не является потокобезопасной. Например, запись данных в транспорт из протокола не является потокобезопасной.»

Я подозреваю, что другие люди, о которых упоминал Жан-Пол, сталкивались с этой проблемой, возможно, совершают аналогичную ошибку. Ответственность за обеспечение выполнения вызовов reactor и других API в правильном потоке лежит на приложении. И, по-видимому, за очень небольшими исключениями, «правильный поток» почти всегда является основным потоком реактора.

Ответ №4:

fork() в Linux определенно оставляет дочернему процессу только один поток.

Я полагаю, вы знаете, что при использовании потоков в Twisted ЕДИНСТВЕННЫМ Twisted API, который разрешено вызывать потокам, является callFromThread? Все другие Twisted API должны вызываться только из основного потока reactor.