使用subprocess模块是否会释放Python GIL锁?

18

通过Python的subprocess模块调用需要相对较长时间的Linux二进制文件,是否会释放GIL?

我想并行化一些调用命令行二进制程序的代码。使用线程(通过threadingmultiprocessing.pool.ThreadPool)还是multiprocessing更好?我的假设是如果subprocess释放了GIL,那么选择threading选项更好。


6
请澄清一下。当前的回答认为你担心子进程本身会以某种方式持有GIL,但我认为你可能担心subprocess.call()subprocess.Popen(...).wait()会阻塞调用者中的其他线程。(它们不会阻塞。) - pilcrow
作为一个从谷歌搜索中发现这个问题的人,我建议你把你的评论变成一个答案,因为与现有的答案不同,它回答了实际的问题。 - Rörd
@Rörd:我已经完成了,谢谢。 - pilcrow
3个回答

17
当通过Python的subprocess模块调用一个运行时间相对较长的Linux二进制文件时,这是否会释放GIL?
是的,在调用过程中,它会释放全局解释器锁(GIL)
如您可能已经了解的那样,在POSIX平台上,subprocess提供了方便的接口,可以在forkexecvewaitpid的基础上进行操作。
通过检查CPython 2.7.9源代码,我们可以发现forkexecve不会释放GIL。但是,这些调用不会阻塞,因此我们不希望GIL被释放。
当然,waitpid会阻塞,但我们可以看到其实现使用ALLOW_THREADS宏来放弃GIL。
static PyObject *
posix_waitpid(PyObject *self, PyObject *args)
{
....
Py_BEGIN_ALLOW_THREADS
pid = waitpid(pid, &status, options);
Py_END_ALLOW_THREADS
....

您可以通过从演示多线程Python脚本调用一些长时间运行的程序,例如sleep来进行测试。


作为一个经验法则,当使用阻塞操作系统API时,比如waitpid(),CPython会释放全局解释器锁(GIL)。 subprocess模块的方法没有什么特别之处。注意:execve()显然会阻塞(在这种情况下是在子进程中)——它仅在出错时返回。 fork()是一个特例:阅读此讨论以了解为什么应避免混合多线程和fork()fork() 立即接着执行 exec() 是可以的)。 - jfs
@J.F.Sebastian:是的,关于经验法则和混合线程和进程的危险性的问题。然而,我对execve()被描述为“阻塞”的说法提出质疑。成功执行execve()并不会阻塞调用者,而是使其消失。 - pilcrow

7

GIL无法跨越多个进程。 subprocess.Popen启动一个新进程。如果它启动了一个Python进程,那么它将有自己的GIL。

如果你只想并行运行一些Linux二进制文件,则不需要多个线程(或由multiprocessing创建的进程):

from subprocess import Popen

# start all processes
processes = [Popen(['program', str(i)]) for i in range(10)]
# now all processes run in parallel

# wait for processes to complete
for p in processes:
    p.wait()

你可以使用 multiprocessing.ThreadPool 来限制同时运行的程序数量

1
@DanqiWang:不是的。multiprocessing提供了基于进程和基于线程的池,两者接口相同。根据情况可以选择使用其中之一。 - jfs
@DanqiWang:Popen启动进程;正如答案中第一段所说,不存在GIL问题。您可以使用from multiprocessing.dummy import Pool(与ThreadPool相同),然后您只需要从导入中删除.dummy,就可以将代码从使用线程更改为使用进程。接口是相同的。 - jfs
1
明白了,没注意到是Popen。我的错。谢谢你的解释。 - Danqi Wang
如果您需要发送数据到进程或检索输出,则此方法无法正常工作。为此,您需要使用communicat()函数,该函数等待进程终止。 - T3rm1
@T3rm1 错了。1-答案中的代码可以直接使用。2-这里是如何调整它以同时从多个进程获得输出的方法。虽然它与GIL无关(在阻塞I/O操作期间会释放)。 - jfs
显示剩余2条评论

1
由于subprocess用于运行可执行文件(实际上它是os.fork()os.execve()的包装器),因此使用它可能更有意义。您可以使用subprocess.Popen。类似这样的代码:
 import subprocess

 process = subprocess.Popen(["binary"])

这将作为一个独立的进程运行,因此不受GIL的影响。然后,您可以使用Popen.poll()方法来检查子进程是否已终止:
if process.poll():
    # process has finished its work
    returncode = process.returncode

请确保不调用任何等待进程完成工作的方法(例如Popen.communicate()),以避免您的Python脚本被阻塞。

this answer所述

multiprocessing用于在现有(Python)代码中运行函数,并支持更灵活的进程之间通信。 multiprocessing模块旨在提供与线程非常相似的接口和功能,同时允许CPython将处理分配给多个CPU /核心,尽管存在GIL。

因此,根据您的用例,subprocess似乎是正确的选择。


1
如果任何子进程填充其stderr管道缓冲区,则process.stdout.readlines()可能会永远阻塞。 如果您想分别读取stdout和stderr,则需要异步方法:线程或非阻塞管道或Windows上的iocp - jfs
完全正确!我忘记了那个。谢谢。 - s16h

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