python 管道 异步读取 stdout select_关于io:python中subprocess.PIPE上的非阻塞读取

这篇博客讨论了在Python中如何非阻塞地读取子进程的stdout,强调了在Windows和Linux上避免死锁的问题。作者提到,使用`subprocess.PIPE`和`.readline()`会导致阻塞,推荐使用线程和队列来实现非阻塞读取。文章还提到了使用`select`和`fcntl`在某些情况下可能无效,并提供了使用`Queue`和线程的示例代码。最后,讨论了线程、非阻塞I/O以及`asyncio`模块在处理子进程输出时的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我正在使用子进程模块启动子进程并连接到它的输出流(stdout)。 我希望能够在其标准输出上执行非阻塞读取。 有没有办法让.readline非阻塞或在我调用.readline之前检查流上是否有数据? 我希望这是可移植的,或至少在Windows和Linux下工作。

这是我现在的工作方式(如果没有数据可用,它会在.readline上阻塞):

p = subprocess.Popen('myprogram.exe', stdout = subprocess.PIPE)

output_str = p.stdout.readline()

(来自谷歌?)当其中一个PIPE的缓冲区被填满而未被读取时,所有PIPE都会死锁。 例如 当stderr被填满时stdout死锁。 永远不要传递你不打算阅读的PIPE。

@ NasserAl-Wohaibi这是否意味着它总能更好地创建文件呢?

我一直很想知道的是为什么它首先阻塞...我问,因为我看到了评论:To avoid deadlocks: careful to: add

to output, flush output, use readline() rather than read()

它是"按设计"等待接收输入。

相关:stackoverflow.com/q/19880190/240515

在这种情况下,fcntl,select,asyncproc将无济于事。

无论操作系统如何,无阻塞地读取流的可靠方法是使用Queue.get_nowait():

import sys

from subprocess import PIPE, Popen

from threading  import Thread

try:

from queue import Queue, Empty

except ImportError:

from Queue import Queue, Empty  # python 2.x

ON_POSIX = 'posix' in sys.builtin_module_names

def enqueue_output(out, queue):

for line in iter(out.readline, b''):

queue.put(line)

out.close()

p = Popen(['myprogram.exe'], stdout=PIPE, bufsize=1, close_fds=ON_POSIX)

q = Queue()

t = Thread(target=enqueue_output, args=(p.stdout, q))

t.daemon = True # thread dies with the program

t.start()

# ... do other things here

# read line without blocking

try:  line = q.get_nowait() # or q.get(timeout=.1)

except Empty:

print('no output yet')

else: # got line

# ... do something with line

是的,这对我有用,我删除了很多。它包括良好实践,但并非总是必要的。 Python 3.x 2.X compat和close_fds可能会被省略,它仍然有效。但只要知道一切都做了什么,不要盲目地复制它,即使它只是有效! (实际上最简单的解决方案是使用一个线程并像Seb那样做一个readline,Qeues只是获取数据的简单方法,还有其他的,线程就是答案!)

在线程内部,对out.readline的调用会阻塞线程和主线程,我必须等到readline返回,然后其他所有内容才会继续。有什么简单的方法吗? (我正在从我的进程中读取多行,这也是另一个正在执行数据库和事物的.py文件)

@Justin:'out.readline'不会阻止它在另一个线程中执行的主线程。

close_fds绝对不是你想盲目复制到你的应用程序中的东西......

如果我无法关闭子进程,例如。由于例外? stdout-reader线程不会死,python会挂起,即使主线程退出,不是吗?怎么可以解决这个问题呢? python 2.x不支持杀死线程,更糟糕的是,不支持中断它们。 :((显然应该处理异常以确保子进程被关闭,但万一它不会,你能做什么?)

@naxa:notice daemon=True:如果退出主线程,python进程将不会挂起。

我在包shelljob pypi.python.org/pypi/shelljob中创建了一些友好的包装器

我在一个单独的线程中启动了Popen,所以我可以在这个线程中"忙等",使用:.... t.start() while p.poll() == None: time.sleep(0.1)在这种情况下,我的GUI不会阻塞(我正在使用TKinter)。所以我也可以使用parent.after(100, self.consume)来模拟事件类型的轮询。在consume方法中,我最终使用q.get()方法从队列中检索数据。奇迹般有效!虽然有些人说你可以使用wait()或poll()与stdout PIPE组合进行死锁?

@ danger89:是的。如果您的父级在.wait()上被阻止但您的孩子等待您阅读其输出(不是您的情况,但GUI回调中的q.get()也不正确),您可能会死锁。最简单的选择是在单独的线程中使用.communicate()。下面是一些代码示例,展示了如何在不"锁定"GUI的情况下从子进程读取输出:1。使用线程2.无线程(POSIX)。如果有什么不清楚,请问

这里close_fds的目的是什么?

@dashesy:避免泄漏父文件描述符。演示的行为是Python 3的默认行为。

@ J.F.Sebastian:有道理。这是唯一没有阻止它的解决方案,似乎在2.7没有其他办法(尝试选择,fcntl)!知道为什么我无法从sgid进程获取输出吗?它在终端工作得很好,但在这里我得到no output yet,这是不可能的。

@dashesy:select,fcntl应该适用于POSIX系统。如果你不明白为什么"没有输出"总是有可能给出给定的解决方案;问一个新问题。请务必阅读如何询问和stackoverflow.com/help/mcve

@ J.F.Sebastian对不起我的绝望尝试,我想你可能已经知道了答案,你看起来像你做的:)我下次会问一个新问题。 BTW,在可执行文件中使用setbuf(stdout, NULL)之后(大多数工作甚至选择和fcntl)就像魅力一样,我不知道为什么没有刷新标准输出但我不会感到惊讶,如果它与它有关suid和selinux。

@dashesy:如果子进程'stdout被重定向到一个管道,那么它是块缓冲的(如果stdout是终端(tty),它是行缓冲的),用于用C编写的基于stdio的程序。参见Python C程序子进程挂起"for it in iter"

@ J.F.Sebastian不知道这一点,天真地我总是假设在tty中看到的行为,但现在它是有道理的。因此,这意味着智能应用程序应该查看stdout的类型,如果它们生成的东西是行并且可以在pipes / grep中使用。

你在enqueue_output中做out.close()的原因是什么?不应该是Popen对象的工作吗?

@zaphod:完成同样的原因with -statement用于普通文件:避免依赖难以理解的垃圾收集来释放资源,即使p.stdout在是垃圾收集:我更喜欢显式的简单和确定性out.close()(虽然我应该在线程中使用with out: - 它似乎适用于Python 2.7和Python 3)。

该解决方案不适用于可能代码系统(16-48核心系统)。 GIL在上下文切换中发挥作用。

如下所述,更好地使用非阻塞IO。

@AntonMedvedev:1。无论有多少CPU都没关系。问题是关于I / O(Python在阻止I / O操作期间发布GIL)。 2.在答案时,便携式解决方案无法替代stdlib中的线程。非阻塞IO是否比线程更好(无条件地)是有争议的。一种明智的方法是研究特定情况下的权衡。

你写'Windows和Linux',这是否排除了OSX?我刚刚在OSX上尝试执行ffmpeg,这似乎有问题。我将更详细地测试这个,除非你告诉我这在OSX中不起作用。

@ P.R。:OP询问这些操作系统,这就是明确提到它们的原因。它也适用于OS X.

@nights:"不适合我"并不是很有用。创建一个最小的代码示例,使用单词描述您期望得到什么以及您逐步获得什么,您是什么操作系统,Python版本并将其作为单独的问题发布。

不要为多处理和多处理切换线程。有这个答案的问题。子进程的终止导致stdout光标移动到我的屏幕的开头。

@jfs是非阻塞I / O必须是异步的吗?

@ uzay95我不确定你在问什么。这些概念密切相关。答案显示了如何在同步阻塞readline()调用之上实现非阻塞读取。该接口也是异步的(执行IO时可能会发生其他事情)。

我觉得我每个月左右偶然发现一次这个帖子......我仍然不知道为什么声称"fcntl在这种情况下不会帮助"。我现在必须使用fnctl设置os.O_NONBLOCK(使用os.read(),而不是readline())实现10-20次。它似乎通常按预期工作。

@cheshirekow是否支持Windows fcntl? Python 2上的readline()是否支持非阻塞模式?

@jfs不确定windows ...你知道吗?这就是为什么你说它不会有帮助?另请注意,我用os.read()而不是readline()说。

os.read()在Windows上可用,但os.O_NONBLOCK被记录为特定于Unix,因此我不希望它在那里工作。

当我尝试使用提供的逻辑回答stderr数据无法得到它

看起来stderr数据也会转到stdout

@Karthi1234没有,stdout在回答管道。 stderr不在这里去管道。你的代码是另一回事。

有没有提到或使用docs.python.org/3/library/asyncio-api-index.html库的原因?这不好吗?

@CharlieParker向下滚动到我的另一个答案

我经常遇到类似的问题;我经常编写的Python程序需要能够执行一些主要功能,同时从命令行(stdin)接受用户输入。简单地将用户输入处理功能放在另一个线程中并不能解决问题,因为readline()会阻塞并且没有超时。如果主要功能已完成并且不再需要等待进一步的用户输入,我通常希望我的程序退出,但它不能,因为readline()仍然在等待一行的另一个线程中阻塞。我发现这个问题的解决方案是使用fcntl模块使stdin成为非阻塞文件:

import fcntl

import os

import sys

# make stdin a non-blocking file

fd = sys.stdin.fileno()

fl = fcntl.fcntl(fd, fcntl.F_GETFL)

fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)

# user input handling thread

while mainThreadIsRunning:

try: input = sys.stdin.readline()

except: continue

handleInput(input)

在我看来,这比使用选择或信号模块解决这个问题要清晰一点,但是它再次只适用于UNIX ...

根据文档,fcntl()可以接收文件描述符或具有.fileno()方法的对象。

杰西的回答是不正确的。根据Guido的说法,readline在非阻塞模式下无法正常工作,并且它不会在Python 3000之前正常工作.bugs.python.org/issue1175#msg56041如果你想使用fcntl将文件设置为非阻塞模式,你必须使用较低级别的os.read()并自己分开行。将fcntl与执行行缓冲的高级调用混合会产生麻烦。

在Python 2中使用readline似乎不正确。请参阅anonnn的回答stackoverflow.com/questions/375427/…

请不要使用繁忙的循环。使用poll()和超时来等待数据。

用read(buffer_size)替换readline似乎完美无缺...

@Stefano什么是buffer_size定义为?

@cat:您的选择,我通常有1024 - 它是一次读取的字节数,所以根据您的预期数据大小设置它更小或更大!

@Stefano是的,我没有意识到它可能是任意文字

有没有提到或使用docs.python.org/3/library/asyncio-api-index.html库的原因?这不好吗?

Python 3.4为异步IO - asyncio模块引入了新的临时API。

该方法类似于@Bryan Ward基于twisted的回答 - 定义协议,并在数据准备就绪后调用其方法:

#!/usr/bin/env python3

import asyncio

import os

class SubprocessProtocol(asyncio.SubprocessProtocol):

def pipe_data_received(self, fd, data):

if fd == 1: # got stdout data (bytes)

print(data)

def connection_lost(self, exc):

loop.stop() # end loop.run_forever()

if os.name == 'nt':

loop = asyncio.ProactorEventLoop() # for subprocess' pipes on Windows

asyncio.set_event_loop(loop)

else:

loop = asyncio.get_event_loop()

try:

loop.run_until_complete(loop.subprocess_exec(SubprocessProtocol,

"myprogram.exe","arg1","arg2"))

loop.run_forever()

finally:

loop.close()

请参阅文档中的"子流程"。

有一个高级接口asyncio.create_subprocess_exec()返回Process对象,允许使用StreamReader.readline()协同程序异步读取一行

(使用async / await Python 3.5+语法):

#!/usr/bin/env python3.5

import asyncio

import locale

import sys

from asyncio.subprocess import PIPE

from contextlib import closing

async def readline_and_kill(*args):

# start child process

process = await asyncio.create_subprocess_exec(*args, stdout=PIPE)

# read line (seq

<think>我们之前已经讨论过`subprocess.Popen.communicate()`卡死的问题,并给出了常见的几个原因和解决方案。现在,用户再次提出这个问题,并引用了一些站内资料,其中涉及在Windows上作为不同用户运行、使用`shell=True`导致挂起以及读取阻塞问题。结合用户的问题和之前的回答,我们需要更具体地针对这些引用内容进行解答。 用户引用内容分析: [1] 提到在Windows上作为不同用户运行子进程。这可能会影响权限,进而导致卡死。 [2] 提到使用`shell=True`时挂起,这可能是由于缓冲区问题或shell启动方式引起。 [3] 提到读取阻塞问题,这与我们之前说的缓冲区死锁一致。 因此,我们将针对这些点,结合之前的解决方案,给出更具体的解决方法。 ### 解决`subprocess.Popen.communicate()`卡死的综合方案 #### 1. **缓冲区死锁(最常见)** **问题**:当子进程的输出量超过操作系统的管道缓冲区大小时,父进程等待子进程结束,而子进程在等待父进程读取输出,造成死锁。 **解决方案**: - 使用线程或异步读取输出流(适用于大量输出) - 使用文件替代管道(适用于输出量极大) **示例代码(使用线程读取)**: ```python import subprocess from threading import Thread def read_stream(stream, buffer): # 逐行读取流直到结束 for line in iter(stream.readline, b''): buffer.append(line) stream.close() proc = subprocess.Popen(['your_command'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1, # 行缓冲 universal_newlines=True) # 文本模式 stdout_buffer, stderr_buffer = [], [] t1 = Thread(target=read_stream, args=(proc.stdout, stdout_buffer)) t2 = Thread(target=read_stream, args=(proc.stderr, stderr_buffer)) t1.start(); t2.start() proc.wait() # 等待子进程结束 t1.join(); t2.join() # 等待线程结束 stdout = ''.join(stdout_buffer) stderr = ''.join(stderr_buffer) ``` #### 2. **子进程等待输入(未关闭标准输入)** **问题**:子进程可能等待标准输入,而父进程没有提供任何输入。 **解决方案**: - 如果不需要输入,显式关闭标准输入: ```python proc = subprocess.Popen(['command'], stdin=subprocess.DEVNULL, # 关闭输入 stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() # 此时不会等待输入 ``` #### 3. **超时处理(防止进程长时间运行)** **问题**:子进程可能执行时间过长或无响应。 **解决方案(Python 3.3+)**: ```python try: proc = subprocess.Popen(['command'], stdout=subprocess.PIPE) stdout, stderr = proc.communicate(timeout=30) # 设置超时时间 except subprocess.TimeoutExpired: proc.kill() # 终止子进程 stdout, stderr = proc.communicate() # 清理资源 ``` #### 4. **在Windows上作为不同用户运行(引用[1])** **问题**:在Windows上以不同用户身份运行子进程时,可能需要特殊权限或配置,否则可能导致进程无法启动或挂起。 **解决方案**: - 使用`runas`命令或Windows API(需要提供用户名和密码) - 注意:在Windows上以不同用户启动进程通常需要提供密码。 **示例(使用`runas`命令)**: ```python # 注意:这种方法需要用户交互输入密码,不适用于自动化 # 如果必须自动化,考虑使用Windows服务或任务计划程序 proc = subprocess.Popen(['runas', '/user:OtherUser', 'command'], stdin=subprocess.PIPE) # 需要手动输入密码,因此不推荐 ``` **替代方案(使用第三方库)**: 使用`pywin32`库可以以指定用户运行进程(需要提供密码): ```python import win32api, win32process, win32security from win32con import LOGON_WITH_PROFILE user = "domain\\username" password = "password" command = "path/to/executable" # 创建进程 handle = win32security.LogonUser(user, None, password, win32security.LOGON32_LOGON_INTERACTIVE, win32security.LOGON32_PROVIDER_DEFAULT) win32process.CreateProcessAsUser(handle, None, command, None, None, False, 0, None, None, win32process.STARTUPINFO()) ``` > **注意**:在代码中存储明文密码存在安全风险,应谨慎使用。 #### 5. **避免使用`shell=True`(引用[2])** **问题**:使用`shell=True`可能引起额外的shell进程,增加死锁风险,且存在安全风险(命令注入)。 **解决方案**: - 尽可能避免使用`shell=True`,直接传递命令和参数列表。 - 如果必须使用(例如需要shell特性),确保正确处理引号和转义。 **示例**: ```python # 不推荐:使用shell=True proc = subprocess.Popen('ls -l', shell=True, stdout=subprocess.PIPE) # 可能挂起 # 推荐:避免shell=True proc = subprocess.Popen(['ls', '-l'], stdout=subprocess.PIPE) # 直接执行 ``` #### 6. **读取阻塞问题(引用[3])** **问题**:直接使用`stdout.read()`或`stderr.read()`可能阻塞,直到流关闭。 **解决方案**: - 使用非阻塞读取(如`select`模块)或线程(如前所述)。 - 使用`communicate()`时,它本身会读取所有数据直到结束,但如果输出过大可能死锁,所以建议使用超时或线程。 ### 总结 `communicate()`卡死通常由缓冲区死锁、子进程等待输入、超时未处理或权限问题引起。针对不同情况,我们可以: 1. 使用线程读取输出流 2. 关闭不需要的标准输入 3. 设置超时并处理超时异常 4. 在Windows上以不同用户运行时使用适当的方法 5. 避免不必要的`shell=True` 6. 对于输出量极大的情况,考虑使用文件 > **特别提示**:在Windows上,管道行为可能与Unix-like系统不同,建议在Windows环境中测试解决方案[^1]。 §§相关问题§§ 1. 如何在Python中安全地执行外部命令(避免命令注入)? 2. 在Windows上如何以管理员身份运行Python子进程? 3. 如何实时获取子进程的输出而不发生阻塞? --相关问题--: 1.如何在Python中安全地执行外部命令(避免命令注入)? 2.在Windows上如何以管理员身份运行Python子进程? 3.如何实时获取子进程的输出而不发生阻塞?</think>### Python subprocess.Popen.communicate() 卡死问题深度解析 `subprocess.Popen.communicate()` 卡死是常见问题,通常由以下原因引起: #### 1. **缓冲区死锁(最常见)** 当子进程输出超过操作系统管道缓冲区(通常 64KB)时,父进程等待子进程结束,而子进程在等待父进程读取输出,造成死锁[^2][^3]。 **解决方案**: ```python import subprocess from threading import Thread def stream_reader(stream, buffer): while True: chunk = stream.read(4096) # 分块读取 if not chunk: break buffer.append(chunk) proc = subprocess.Popen(['your_command'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0) # 无缓冲模式 stdout_buf, stderr_buf = [], [] t1 = Thread(target=stream_reader, args=(proc.stdout, stdout_buf)) t2 = Thread(target=stream_reader, args=(proc.stderr, stderr_buf)) t1.start(); t2.start() proc.wait() # 等待进程结束 t1.join(); t2.join() # 等待读取线程结束 stdout = b''.join(stdout_buf).decode() stderr = b''.join(stderr_buf).decode() ``` #### 2. **子进程等待输入** 当子进程需要 STDIN 输入但未收到时[^3]: ```python # 明确关闭输入流 proc = subprocess.Popen(['cmd'], stdin=subprocess.DEVNULL, # 显式关闭输入 stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = proc.communicate() ``` #### 3. **Windows 特定问题** 在 Windows 上作为不同用户运行时[^1]: ```python import win32api, win32process, win32security user = "domain\\username" password = "password" command_line = "your_command" # 创建安全令牌 handle = win32security.LogonUser( user, None, password, win32security.LOGON32_LOGON_INTERACTIVE, win32security.LOGON32_PROVIDER_DEFAULT ) # 创建进程 startup = win32process.STARTUPINFO() proc_info = win32process.CreateProcessAsUser( handle, None, command_line, None, None, False, 0, None, None, startup ) win32api.CloseHandle(handle) ``` #### 4. **超时处理(Python 3.3+)** ```python try: proc = subprocess.Popen(['slow_command'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) outs, errs = proc.communicate(timeout=30) # 设置超时 except subprocess.TimeoutExpired: proc.kill() # 强制终止 outs, errs = proc.communicate() # 清理资源 ``` #### 5. **输出量极大时的优化** 使用临时文件避免管道阻塞: ```python import tempfile with tempfile.TemporaryFile() as outfile, \ tempfile.TemporaryFile() as errfile: proc = subprocess.Popen(['high_output_command'], stdout=outfile, stderr=errfile) proc.wait() outfile.seek(0) errfile.seek(0) stdout = outfile.read().decode() stderr = errfile.read().decode() ``` ### 关键预防措施 1. **避免 `shell=True`**:除非必要,否则显式传递参数列表[^2] ```python # 危险做法 Popen('ls -l | grep py', shell=True) # 安全做法 Popen(['ls', '-l'], stdout=PIPE) ``` 2. **实时读取输出**:对于长时间进程,使用增量读取 ```python proc = Popen(['cmd'], stdout=PIPE, text=True) while proc.poll() is None: print(proc.stdout.readline(), end='') ``` 3. **检查进程状态**: ```python if proc.poll() is None: # 仍在运行 print("Process is hanging") ``` > **诊断工具**:在 Linux 使用 `strace -p <PID>` 分析系统调用,Windows 使用 Process Monitor[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值