使用子进程、pty 和线程池的死锁
deadlock using subprocess, pty, and threadpools
我有一个特殊情况,我想伪造一个 tty 到 ThreadPoolExecutor
中 运行 的子进程(想想 xargs -p
)并捕获输出。
我创建了以下似乎连续运行良好的:
import contextlib
import concurrent.futures
import errno
import os
import subprocess
import termios
@contextlib.contextmanager
def pty():
r, w = os.openpty()
try:
yield r, w
finally:
for fd in r, w:
try:
os.close(fd)
except OSError:
pass
def cmd_output_p(*cmd):
with pty() as (r, w):
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=w, stderr=w)
os.close(w)
proc.stdin.close()
buf = b''
while True:
try:
bts = os.read(r, 1024)
except OSError as e:
if e.errno == errno.EIO:
bts = b''
else:
raise
else:
buf += bts
if not bts:
break
return proc.wait(), buf
和一些示例用法:
>>> cmd_output_p('python', '-c', 'import sys; print(sys.stdout.isatty())')
(0, b'True\r\n')
太棒了!但是,当我 运行 在 concurrent.futures.ThreadPoolExecutor
中使用相同的过程时,会出现很多故障模式(还有一种更罕见的故障模式,其中 OSError: [Errno 9] Bad file descriptor
出现在看似随机的代码行上——但是我还没有为此分离出复制品)
例如下面的代码:
def run(arg):
print(cmd_output_p('echo', arg))
with concurrent.futures.ThreadPoolExecutor(5) as exe:
exe.map(run, ('1',) * 1000)
这会在一堆进程中突然发生,然后最终挂起。发出 ^C
给出以下堆栈跟踪(^C 需要两次才能结束该过程)
$ python3 t.py
...
(0, b'1\r\n')
(0, b'1\r\n')
(0, b'1\r\n')
^CTraceback (most recent call last):
File "t.py", line 49, in <module>
exe.map(run, ('1',) * 1000)
File "/usr/lib/python3.6/concurrent/futures/_base.py", line 611, in __exit__
self.shutdown(wait=True)
File "/usr/lib/python3.6/concurrent/futures/thread.py", line 152, in shutdown
t.join()
File "/usr/lib/python3.6/threading.py", line 1056, in join
self._wait_for_tstate_lock()
File "/usr/lib/python3.6/threading.py", line 1072, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
KeyboardInterrupt
^CError in atexit._run_exitfuncs:
Traceback (most recent call last):
File "/usr/lib/python3.6/concurrent/futures/thread.py", line 40, in _python_exit
t.join()
File "/usr/lib/python3.6/threading.py", line 1056, in join
self._wait_for_tstate_lock()
File "/usr/lib/python3.6/threading.py", line 1072, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
KeyboardInterrupt
大概是我做错了什么,但我发现的所有与子进程/pty 交互的示例都执行相同的步骤——如何防止这种死锁?
在 cmd_output_p
和 pty
中对 os.close
的调用之间,一个线程以 w
打开的文件描述符可能会被另一个线程重用。在这种情况下,它会被第二个 close
意外关闭(否则会因 EBADF
而“无害地”失败)。 (几乎在任何多线程程序中,EBADF
都必须被视为断言失败。)
这肯定会导致其他线程的 EBADF
。如果重新打开文件描述符,它也可能导致死锁,因为伪终端的两端都是可读写的。
我有一个特殊情况,我想伪造一个 tty 到 ThreadPoolExecutor
中 运行 的子进程(想想 xargs -p
)并捕获输出。
我创建了以下似乎连续运行良好的:
import contextlib
import concurrent.futures
import errno
import os
import subprocess
import termios
@contextlib.contextmanager
def pty():
r, w = os.openpty()
try:
yield r, w
finally:
for fd in r, w:
try:
os.close(fd)
except OSError:
pass
def cmd_output_p(*cmd):
with pty() as (r, w):
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=w, stderr=w)
os.close(w)
proc.stdin.close()
buf = b''
while True:
try:
bts = os.read(r, 1024)
except OSError as e:
if e.errno == errno.EIO:
bts = b''
else:
raise
else:
buf += bts
if not bts:
break
return proc.wait(), buf
和一些示例用法:
>>> cmd_output_p('python', '-c', 'import sys; print(sys.stdout.isatty())')
(0, b'True\r\n')
太棒了!但是,当我 运行 在 concurrent.futures.ThreadPoolExecutor
中使用相同的过程时,会出现很多故障模式(还有一种更罕见的故障模式,其中 OSError: [Errno 9] Bad file descriptor
出现在看似随机的代码行上——但是我还没有为此分离出复制品)
例如下面的代码:
def run(arg):
print(cmd_output_p('echo', arg))
with concurrent.futures.ThreadPoolExecutor(5) as exe:
exe.map(run, ('1',) * 1000)
这会在一堆进程中突然发生,然后最终挂起。发出 ^C
给出以下堆栈跟踪(^C 需要两次才能结束该过程)
$ python3 t.py
...
(0, b'1\r\n')
(0, b'1\r\n')
(0, b'1\r\n')
^CTraceback (most recent call last):
File "t.py", line 49, in <module>
exe.map(run, ('1',) * 1000)
File "/usr/lib/python3.6/concurrent/futures/_base.py", line 611, in __exit__
self.shutdown(wait=True)
File "/usr/lib/python3.6/concurrent/futures/thread.py", line 152, in shutdown
t.join()
File "/usr/lib/python3.6/threading.py", line 1056, in join
self._wait_for_tstate_lock()
File "/usr/lib/python3.6/threading.py", line 1072, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
KeyboardInterrupt
^CError in atexit._run_exitfuncs:
Traceback (most recent call last):
File "/usr/lib/python3.6/concurrent/futures/thread.py", line 40, in _python_exit
t.join()
File "/usr/lib/python3.6/threading.py", line 1056, in join
self._wait_for_tstate_lock()
File "/usr/lib/python3.6/threading.py", line 1072, in _wait_for_tstate_lock
elif lock.acquire(block, timeout):
KeyboardInterrupt
大概是我做错了什么,但我发现的所有与子进程/pty 交互的示例都执行相同的步骤——如何防止这种死锁?
在 cmd_output_p
和 pty
中对 os.close
的调用之间,一个线程以 w
打开的文件描述符可能会被另一个线程重用。在这种情况下,它会被第二个 close
意外关闭(否则会因 EBADF
而“无害地”失败)。 (几乎在任何多线程程序中,EBADF
都必须被视为断言失败。)
这肯定会导致其他线程的 EBADF
。如果重新打开文件描述符,它也可能导致死锁,因为伪终端的两端都是可读写的。