Для ниже кода, запущенного в Win7, есть потоки T1 и T2, T1 печатает содержимое dir в исходном окне и T2 ping в течение 4 секунд в новом окне.
import os
import sys
import logging
import subprocess
import threading
class T1 (threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
proc = subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE)
for line in iter(proc.stdout.readline, ''):
logging.debug(line)
logging.info("HEREEEEEEEE")
class T2 (threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
subprocess.Popen(["ping.exe", "-n", "4", "127.0.0.1"], creationflags=subprocess.CREATE_NEW_CONSOLE)
logging.info("")
if __name__=='__main__':
logger = logging.getLogger('root')
FORMAT = "[TID:%(thread)d %(funcName)s L#%(lineno)s] %(message)s"
logging.basicConfig(format=FORMAT, level=logging.DEBUG)
t1 = T1()
t2 = T2()
t1.start()
t2.start()
t1.join()
t2.join()
sys.exit(0)
Для строки logging.info("HEREEEEEEEE")
в потоке T1, я думаю, она должна быть напечатана сразу после logging.info("HEREEEEEEEE")
.
Что для меня не имеет смысла: почему строка не печатается сразу, а печатается через 4 секунды после завершения потока T2?
Интересно, связано ли это с файловым дескриптором в многопоточности.
Для Python 2 в вашем коде есть условие гонки, которое может протечь наследуемый конец записи, созданный потоком T1
в процесс ping.exe, созданный потоком T2
. readline
на трубе не вернется, пока труба не закроется, что требует, чтобы все ручки для конца записи были закрыты.
В этом случае вы можете избежать состояния гонки, передав close_fds=True
в Popen
при создании процесса ping.exe. Это предотвращает его наследование наследуемых ручек, включая дескриптор канала из перекрывающегося вызова в потоке T1
.
В общем, до Python 3.7, если вам нужно поддерживать одновременные вызовы Popen
с переопределенными стандартными дескрипторами, вам нужно будет обернуть Popen
функцией, которая синхронизирует вызов, предварительно закрепив блокировку. К сожалению, это приводит к тому, что Popen
называют узким местом в многопоточном процессе, но нет простой альтернативы.
Фон
В Unix параметр close_fds
из Popen
применяется в дочернем процессе после fork
. Если это правда, то все нестандартные дескрипторы файлов будут закрыты перед вызовом exec
, даже без флага FD_CLOEXEC
. Однако он не закрывает стандартные дескрипторы файлов - stdin (0), stdout (1) и stderr (2).
В Windows дескриптор может быть помечен как наследуемый (то есть HANDLE_FLAG_INHERIT
). По умолчанию дескрипторы не наследуются. Если CreateProcess
вызывается с bInheritHandles
как true, то все наследуемые дескрипторы наследуются дочерним bInheritHandles
. Popen
передает это как not close_fds
, т. not close_fds
Не закрывая дескрипторы файла, означает наследовать дескрипторы [*].
В Windows параметры stdin
, stdout
и stderr
Popen
используются для явного задания дочерних стандартных дескрипторов в STARTUPINFO
(т.е. hStdInput
, hStdOutput
, hStdError
). Если стандартные дескрипторы явно не переопределены, родительские стандартные дескрипторы неявно унаследованы консольными приложениями (например, python.exe), но не приложениями графического интерфейса (например, pythonw.exe). Если задано явно, ручки должны быть наследуемыми, а bInheritHandles
(т. not close_fds
) должны быть истинными. Это источник состояния гонки, когда другой поток выполняет перекрывающий вызов CreateProcess
который также наследует дескрипторы.
В Python 3 частота этого состояния гонки уменьшается, если значение close_fds
true, если стандартные дескрипторы не переопределены. В 3.7, он смягчается далее путем пропускания стандартных ручек в lpAttributeList
области STARTUPINFOEX
. При этом изменении одновременные вызовы Popen
могут переопределять стандартные дескрипторы без утечки дескрипторов. Тем не менее, дескрипторы все еще должны быть наследуемыми, поэтому есть еще условие гонки с одновременными вызовами других функций, которые наследуют дескрипторы, такие как os.system
и os.spawnl
.
[*] Обратите внимание, что, несмотря на имя параметра, дескрипторы файлов C 'на самом деле не унаследованы в Windows. В то время как среда выполнения C может наследовать файловые дескрипторы для system
и функции spawn
/exec
, ее использование STARTUPINFO
для реализации этого недокументировано. Таким образом, Popen
наследует только дескрипторы. При наследовании нестандартного дескриптора вам необходимо передать значение дескриптора msvcrt.get_osfhandle
(например, через stdin, командную строку или переменную окружения), которую вы можете получить через msvcrt.get_osfhandle
. Ребенок может открыть новый файловый дескриптор для унаследованного дескриптора через msvcrt.open_osfhandle
.