使用Python子进程进行IPC

5
我正在尝试用Python进行简单的IPC,步骤如下:一个Python进程使用subprocess启动另一个进程。子进程将一些数据发送到管道中,父进程接收它。
以下是我当前的实现方式:
# parent.py
import pickle
import os
import subprocess
import sys
read_fd, write_fd = os.pipe()
if hasattr(os, 'set_inheritable'):
    os.set_inheritable(write_fd, True)
child = subprocess.Popen((sys.executable, 'child.py', str(write_fd)), close_fds=False)
try:
    with os.fdopen(read_fd, 'rb') as reader:
        data = pickle.load(reader)
finally:
    child.wait()
assert data == 'This is the data.'

# child.py
import pickle
import os
import sys
with os.fdopen(int(sys.argv[1]), 'wb') as writer:
    pickle.dump('This is the data.', writer)

在Unix上,这段代码按预期工作,但如果我在Windows上运行此代码,则会出现以下错误,之后程序一直挂起直到被中断:
Traceback (most recent call last):
  File "child.py", line 4, in <module>
    with os.fdopen(int(sys.argv[1]), 'wb') as writer:
  File "C:\Python34\lib\os.py", line 978, in fdopen
    return io.open(fd, *args, **kwargs)
OSError: [Errno 9] Bad file descriptor

我怀疑问题在于子进程没有继承write_fd文件描述符。我该如何解决?
这段代码需要与Python 2.7、3.2和以后的所有版本兼容。这意味着解决方案不能依赖于PEP 446中指定的文件描述符继承更改的存在或缺失。正如上面所暗示的,它还需要在Unix和Windows上运行。
(回答一些显而易见的问题:我之所以不使用multiprocessing,是因为在我的实际非简化代码中,两个Python程序是Django项目的一部分,具有不同的设置模块。这意味着它们不能共享任何全局状态。此外,子进程的标准流正在用于其他目的,因此不可用于此。)
更新:在设置了close_fds参数后,该代码现在可以在Unix上的所有Python版本中运行。然而,在Windows上仍然失败。

这并不是直接回答你的问题,但是execnet可以做到这一点。 - Steven Kryskalla
我更喜欢不引入额外依赖的解决方案,特别是因为我已经有了大部分的依赖。但如果没有其他选择,我可能会选择这个。 - Taymon
也许你可以深入研究他们的实现方式,看看他们是如何做到的。它适用于Linux和Windows操作系统。 - Steven Kryskalla
2个回答

3

subprocess.PIPE适用于所有平台,为什么不直接使用它呢?

如果您想手动创建和使用os.pipe(),那么需要注意Windows不支持fork()。相反,它使用的是CreateProcess(),默认情况下不会使子进程继承打开的文件。但是有一种方法:每个单独的文件描述符都可以被明确地继承。这需要调用Win API。我在gipc中实现了这个功能,请查看这里_pre/post_createprocess_windows()方法。


2

@Jan-Philip Gehrcke所建议的那样,您可以使用subprocess.PIPE代替os.pipe()

#!/usr/bin/env python
# parent.py
import sys
from subprocess import check_output

data = check_output([sys.executable or 'python', 'child.py'])
assert data.decode().strip() == 'This is the data.'

check_output() 在内部使用 stdout=subprocess.PIPE

如果 child.py 使用 data = pickle.dumps(obj),则可以使用 obj = pickle.loads(data)

而且,child.py 可以简化为:

#!/usr/bin/env python
# child.py
print('This is the data.')

如果子进程是用Python编写的,为了更灵活,您可以将子脚本作为模块导入并调用其函数,而不是使用subprocess。如果需要在不同进程中运行一些Python代码,则可以使用multiprocessing、concurrent.futures模块。
如果无法使用标准流,则django应用程序可以使用套接字进行通信。
引用:“我不使用multiprocessing的原因是,在我的实际非简化代码中,两个Python程序是具有不同设置模块的Django项目的一部分。这意味着它们不能共享任何全局状态。”这似乎是虚假的。multiprocessing在幕后也可能使用subprocess模块。如果您不想共享全局状态,请不要共享它-这是多个进程的默认设置。您应该针对您的特定情况提出更具体的问题,询问如何组织项目各部分之间的通信。

我所指的全局状态是在任何已直接或间接导入Django设置的导入模块中。我尝试创建一个multiprocessing进程来更改DJANGO_SETTINGS_MODULE环境变量的值,然后导入Django,但这并没有起作用,因为Django已经在子进程中被导入,在我的代码有机会运行之前。 - Taymon
@Taymon:评论可能不是回答的合适位置,这就是为什么我建议提出一个新问题。但我看到了一个微不足道的解决方法:不要在主进程中过早导入,而是在子进程的初始化器中进行,设置os.environ并在那里导入必要的内容:唯一受影响的代码是生成多个进程的代码,其余部分都是相同的。 - jfs

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