Python子进程在接收stdin EOF时出现神秘延迟

3 投票
1 回答
2005 浏览
提问于 2025-04-16 11:29

我把我在应用程序中遇到的问题简化成了下面这个测试案例。在这段代码里,一个父进程同时启动了两个(你可以启动更多)子进程,这些子进程从父进程那里通过标准输入读取一条大消息,然后睡眠5秒钟,最后再写点东西回来。不过,代码的执行时间却意外地变成了10秒,而不是预期的5秒。

如果你把verbose=True设置为真,你会发现那个慢吞吞的子进程接收到了大部分消息,然后在等待最后3个字符的到来——它没有检测到管道已经关闭。此外,如果我干脆不对第二个进程做任何操作(doreturn=True),第一个进程将永远看不到结束信号(EOF)。

有没有人知道这是怎么回事?下面是一些示例输出。提前谢谢你。

from subprocess import *
from threading import *
from time import *
from traceback import *
import sys
verbose = False
doreturn = False
msg = (20*4096+3)*'a'
def elapsed(): return '%7.3f' % (time() - start)
if sys.argv[1:]:
  start = float(sys.argv[2])
  if verbose:
    for chunk in iter(lambda: sys.stdin.read(4096), ''):
      print >> sys.stderr, '..', time(), sys.argv[1], 'read', len(chunk)
  else:
    sys.stdin.read()
  print >> sys.stderr, elapsed(), '..', sys.argv[1], 'done reading'
  sleep(5)
  print msg
else:
  start = time()
  def go(i):
    print elapsed(), i, 'starting'
    p = Popen(['python','stuckproc.py',str(i), str(start)], stdin=PIPE, stdout=PIPE)
    if doreturn and i == 1: return
    print elapsed(), i, 'writing'
    p.stdin.write(msg)
    print elapsed(), i, 'closing'
    p.stdin.close()
    print elapsed(), i, 'reading'
    p.stdout.read()
    print elapsed(), i, 'done'
  ts = [Thread(target=go, args=(i,)) for i in xrange(2)]
  for t in ts: t.start()
  for t in ts: t.join()

示例输出:

  0.001 0 starting
  0.003 1 starting
  0.005 0 writing
  0.016 1 writing
  0.093 0 closing
  0.093 0 reading
  0.094 1 closing
  0.094 1 reading
  0.098 .. 1 done reading
  5.103 1 done
  5.108 .. 0 done reading
 10.113 0 done

我使用的是Python 2.6.5,如果这有影响的话。

相关问题:

1 个回答

6

经过很长时间的摸索,我终于搞明白了,灵感来自于这篇帖子中的一句话:

查看pipe(7)中的“I/O on Pipes and FIFOs”部分(可以用命令“man 7 pipe”查看)

“如果所有指向管道写入端的文件描述符都已经关闭,那么尝试从管道读取数据时会看到文件结束(read(2)会返回0)。”

我本该知道这一点,但当时没想到,这和Python没有直接关系。发生的事情是:子进程在相互的管道中打开了(写入)文件描述符。只要管道中还有打开的写入文件描述符,读取者就不会看到文件结束。

举个例子:

p1=Popen(..., stdin=PIPE, ...) # creates a pipe the parent process can write to
p2=Popen(...) # inherits the writer FD - as long as p2 exists, p1 won't see EOF

结果发现,Popen有一个close_fds参数,所以解决办法就是把close_fds=True传进去。事后看来这一切都简单明了,但还是浪费了不少时间。

撰写回答