为什么subprocess.Popen不会等待子进程终止?

6

我遇到了Python的subprocess.Popen方法的问题。

这是一个测试脚本,演示了问题。它正在Linux系统上运行。

#!/usr/bin/env python
import subprocess
import time

def run(cmd):
  p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
  return p

### START MAIN
# copy some rows from a source table to a destination table
# note that the destination table is empty when this script is run
cmd = 'mysql -u ve --skip-column-names --batch --execute="insert into destination (select * from source limit 100000)" test'
run(cmd)

# check to see how many rows exist in the destination table
cmd = 'mysql -u ve --skip-column-names --batch --execute="select count(*) from destination" test'
process = run(cmd)
count = (int(process.communicate()[0][:-1]))

# if subprocess.Popen() waited for the child to terminate than count should be
# greater than 0
if count > 0:
  print "success: " + str(count)
else:
  print "failure: " + str(count)
  time.sleep(5)

  # find out how many rows exists in the destination table after sleeping
  process = run(cmd)
  count = (int(process.communicate()[0][:-1]))
  print "after sleeping the count is " + str(count)

通常这个脚本的输出是:
success: 100000

但有时候这会变得

failure: 0
after sleeping the count is 100000

请注意,在失败的情况下,插入操作后立即执行的选择查询显示零行,但在休眠5秒钟后,第二个选择正确地显示了100,000行的计数。我的结论是以下之一为真:
  1. subprocess.Popen没有等待子线程终止-这似乎与文档相矛盾
  2. mysql插入不是原子性的-我对mysql的理解表明插入是原子性的
  3. 选择查询没有立即看到正确的行数-根据一个比我更了解mysql的朋友,这也不应该发生
你还需要知道什么?
顺便提一句,我知道这是一种从Python与MySQL交互的hacky方法,而MySQLdb可能不会有这个问题,但我很好奇为什么这种方法不起作用。

感谢大家提供的出色答案。我再次查看了子进程文档,发现我被“等待命令完成”这条注释弄糊涂了,这个注释出现在方便方法部分而不是Popen方法部分。我选择了Jed的答案,因为它最好地回答了我的原始问题,但我认为我会在将来的脚本需求中使用Paul的解决方案。 - Drew Sherman
请记住,os.system(除非您对其进行其他操作)返回进程的返回值(通常为0或1)。也不要让它咬到你。 - Paul McMillan
3个回答

21

subprocess.Popen在实例化时会运行程序。然而,它不会等待它完成——它会像在shell中输入cmd &一样在后台启动。因此,在上面的代码中,您基本上定义了一个竞争条件——如果插入能够及时完成,它将显示正常,但如果不能,则会得到意外的输出。您没有等待第一个run()的PID完成,而是仅返回其Popen实例并继续执行。

我不确定这种行为是否与文档相矛盾,因为Popen有一些非常明确的方法表明它不需要等待,例如:

Popen.wait()
  Wait for child process to terminate. Set and return returncode attribute.

我同意,然而,这个模块的文档可以改进。

为了等待程序完成,我建议使用 subprocess 的便捷方法 subprocess.call,或者在需要 stdout 的情况下使用 Popen 对象上的 communicate 方法(针对第二次调用时已经进行了这样的操作)。

### START MAIN
# copy some rows from a source table to a destination table
# note that the destination table is empty when this script is run
cmd = 'mysql -u ve --skip-column-names --batch --execute="insert into destination (select * from source limit 100000)" test'
subprocess.call(cmd)

# check to see how many rows exist in the destination table
cmd = 'mysql -u ve --skip-column-names --batch --execute="select count(*) from destination" test'
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
try: count = (int(process.communicate()[0][:-1]))
except: count = 0

此外,在大多数情况下,您不需要在shell中运行命令。这是其中之一,但是您需要像序列一样重写命令。以这种方式执行还可以避免传统的Shell注入,并减少对引号的担忧,如下所示:

prog = ["mysql", "-u", "ve", "--execute", 'insert into foo values ("snargle", 2)']
subprocess.call(prog)
这甚至会起作用,并且不会像您期望的那样注入:
prog = ["printf", "%s", "<", "/etc/passwd"]
subprocess.call(prog)

可以交互式尝试。这样可以避免shell注入的可能性,特别是如果你正在接受用户输入。我猜想你之所以使用较不出色的字符串方法与子进程通信,是因为在获取序列工作时遇到了麻烦 :^)


2
我正在使用subprocess.call,但它似乎也不等待。紧接着的语句告诉代码删除刚刚运行的文件,但在代码运行之前就被调用了,导致程序崩溃。 - Elliot

8

如果您不是非常需要使用subprocess和popen,通常更简单的方法是使用os.system。例如,对于快速脚本,我经常做以下操作:

import os
run = os.system #convenience alias
result = run('mysql -u ve --execute="select * from wherever" test')

与popen不同,os.system会等待你的进程返回后才会继续执行脚本的下一阶段。更多信息可以在文档中查看:http://docs.python.org/library/os.html#os.system

3
伙计,为什么你认为subprocess.Popen返回一个带有wait方法的对象呢?除非等待是隐含、内在、立即和不可避免的,就像你所推测的那样...?! 最常见的生成子进程的原因不是立即等待它完成,而是让它同时运行(例如在另一个核心上,或者最坏情况下通过时间分片——这是操作系统和硬件的事情),而父进程继续运行。当父进程需要等待子进程完成时,它显然会在由原始subprocess.Process调用返回的对象上调用wait

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接