Производительность subprocess.check_output vs subprocess.call
Я использовал subprocess.check_output()
в течение некоторого времени для захвата вывода из подпроцессов, но при определенных обстоятельствах столкнулся с некоторыми проблемами производительности. Я запускаю это на машине RHEL6.
Вызывающая среда Python скомпилирована в Linux и 64-битная. Подпроцессом, который я выполняю, является оболочка script, которая в конечном итоге запускает процесс python.exe Windows через Wine (почему эта глупость требуется, это еще одна история). В качестве вклада в оболочку script, я пишу небольшую часть кода Python, который передается в файл python.exe.
В то время как система находится под умеренной/большой нагрузкой (от 40 до 70% загрузки процессора), я заметил, что использование subprocess.check_output(cmd, shell=True)
может привести к значительной задержке (до ~ 45 секунд) после завершения выполнения подпроцесса возвращается команда check_output. Глядя на вывод из ps -efH
в течение этого времени, вызывается вызываемый подпроцесс как sh <defunct>
, пока он не вернется с нормальным нулевым статусом выхода.
И наоборот, использование subprocess.call(cmd, shell=True)
для запуска той же команды при той же средней/тяжелой нагрузке приведет к немедленному возврату подпроцесса без задержки, все выходные данные будут напечатаны в STDOUT/STDERR (а не возвращены из вызова функции).
Почему существует такая значительная задержка только тогда, когда check_output()
перенаправляет вывод STDOUT/STDERR в его возвращаемое значение, а не когда call()
просто возвращает его обратно родительскому STDOUT/STDERR?
Ответы
Ответ 1
Чтение документов, как subprocess.call
, так и subprocess.check_output
являются примерами использования subprocess.Popen
. Одно из незначительных отличий заключается в том, что check_output
вызывает ошибку Python, если подпроцесс возвращает ненулевой статус выхода. Большая разница подчеркивается в бит около check_output
(мой акцент):
Полнофункциональная подпись во многом такая же, как и у конструктора Popen, за исключением того, что stdout не разрешается, поскольку он используется внутри. Все остальные аргументы передаются непосредственно в конструктор Popen.
Итак, как stdout
"используется внутри"? Давайте сравним call
и check_output
:
Вызов
def call(*popenargs, **kwargs):
return Popen(*popenargs, **kwargs).wait()
check_output
def check_output(*popenargs, **kwargs):
if 'stdout' in kwargs:
raise ValueError('stdout argument not allowed, it will be overridden.')
process = Popen(stdout=PIPE, *popenargs, **kwargs)
output, unused_err = process.communicate()
retcode = process.poll()
if retcode:
cmd = kwargs.get("args")
if cmd is None:
cmd = popenargs[0]
raise CalledProcessError(retcode, cmd, output=output)
return output
общаются h2 >
Теперь мы должны посмотреть на Popen.communicate
. Выполняя это, мы замечаем, что для одного канала communicate
выполняет несколько действий, которые просто занимают больше времени, чем просто возврат Popen().wait()
, как это делает call
.
С одной стороны, communicate
обрабатывает stdout=PIPE
, установлен ли shell=True
или нет. Ясно, что call
нет. Это просто позволяет вашей оболочке использовать все, что... угроза безопасности, как здесь описывает Python.
Во-вторых, в случае check_output(cmd, shell=True)
(всего один канал)... независимо от того, какой ваш подпроцесс отправляет на stdout
, обрабатывается потоком в методе _communicate
. И Popen
должен присоединиться к потоку (ждать на нем), прежде чем дожидаться завершения самого подпроцесса!
Плюс, более тривиально, он обрабатывает stdout
как list
, который затем должен быть присоединен к строке.
Короче говоря, даже при минимальных аргументах check_output
тратит намного больше времени на процессы Python, чем call
делает.
Ответ 2
Посмотрите на код. У .check_output следующее ожидание:
def _internal_poll(self, _deadstate=None, _waitpid=os.waitpid,
_WNOHANG=os.WNOHANG, _os_error=os.error, _ECHILD=errno.ECHILD):
"""Check if child process has terminated. Returns returncode
attribute.
This method is called by __del__, so it cannot reference anything
outside of the local scope (nor can any methods it calls).
"""
if self.returncode is None:
try:
pid, sts = _waitpid(self.pid, _WNOHANG)
if pid == self.pid:
self._handle_exitstatus(sts)
except _os_error as e:
if _deadstate is not None:
self.returncode = _deadstate
if e.errno == _ECHILD:
# This happens if SIGCLD is set to be ignored or
# waiting for child processes has otherwise been
# disabled for our process. This child is dead, we
# can't get the status.
# http://bugs.python.org/issue15756
self.returncode = 0
return self.returncode
.call ждет, используя следующий код:
def wait(self):
"""Wait for child process to terminate. Returns returncode
attribute."""
while self.returncode is None:
try:
pid, sts = _eintr_retry_call(os.waitpid, self.pid, 0)
except OSError as e:
if e.errno != errno.ECHILD:
raise
# This happens if SIGCLD is set to be ignored or waiting
# for child processes has otherwise been disabled for our
# process. This child is dead, we can't get the status.
pid = self.pid
sts = 0
# Check the pid and loop as waitpid has been known to return
# 0 even without WNOHANG in odd situations. issue14396.
if pid == self.pid:
self._handle_exitstatus(sts)
return self.returncode
Обратите внимание, что ошибка связана с internal_poll. Он доступен для просмотра http://bugs.python.org/issue15756. В значительной степени именно проблема, с которой вы сталкиваетесь.
Изменить: Другая потенциальная проблема между .call и .check_output заключается в том, что .check_output действительно заботится о stdin и stdout и будет пытаться выполнить IO для обеих труб. Если вы работаете с процессом, который попадает в состояние зомби, возможно, что чтение против трубы в состоянии, несущем состояние, вызывает зависание, которое вы испытываете.
В большинстве случаев состояния зомби довольно быстро очищаются, но они не будут, если, например, они прерываются при системном вызове (например, чтение или запись). Конечно, системный вызов чтения/записи сам должен быть прерван, как только IO больше не будет выполняться, но, возможно, вы попадаете в какое-то состояние гонки, когда вещи убиваются в плохом порядке.
Единственный способ, который я могу придумать, чтобы определить, какая причина в этом случае, заключается в том, чтобы вы либо добавляли код отладки в файл подпроцесса, либо вызывали отладчик python и инициировали обратную трассировку, когда вы запускаете условие, в котором вы находитесь испытывают.