在Python 3 Web应用程序中,我需要使用命令行实用程序来处理图像,将其输出写入命名管道(fifo),然后将该输出(管道内容)解析为PIL /枕头图像。这是基本流程(工作代码很长,没有错误!):
from os import mkfifo
from os import unlink
from PIL import Image
from subprocess import DEVNULL
from subprocess import PIPE
from subprocess import Popen
fifo_path = '/tmp/myfifo.bmp'
cmd = '/usr/bin/convert -resize 100 /path/to/some.tif ' + fifo_path
# make a named pipe
mkfifo(fifo_path)
# execute
proc = Popen(cmd, stdout=DEVNULL, stderr=PIPE, shell=True)
# parse the image
pillow_image = Image.open(fifo_path)
# finish the process:
proc_exit = proc.wait()
# remove the pipe:
unlink(fifo_path)
# just for proof:
pillow_image.show()
(我在上面的示例中替换了我实际上必须使用ImageMagick的实用程序,只是因为你不太可能拥有它 - 它根本不会影响问题。)
这在大多数情况下效果很好,我可以处理大多数例外情况(为清楚起见,上面省略了),但有一个案例我无法设法解决如何处理,这是什么如果在shellout中出现问题,会导致出现空管道,例如如果图像由于某种原因不存在或已损坏,例如:
fifo_path = '/tmp/myfifo.bmp'
cmd = '/usr/bin/convert -resize 100 /path/to/some/bad_or_missing.tif ' + fifo_path
# make a named pipe
mkfifo(fifo_path)
# execute
proc = Popen(cmd, stdout=DEVNULL, stderr=PIPE, shell=True)
# parse the image
pillow_image = Image.open(fifo_path) # STUCK
...
应用程序只是挂在这里,因为我无法访问proc_exit = proc.wait()
我无法设置timeout
(例如proc_exit = proc.wait(timeout=2)
),这就是我&# 39; d通常这样做。
我已尝试将整个业务包装在上下文管理器中,类似于this answer,但该配方不是线程安全的,这是一个问题,我无法找到线程或多处理解决方案,当我加入线程或进程时,我可以访问PIL / Pillow Image实例(不是我的强力套装,而是类似的东西):
from multiprocessing import Process
from os import mkfifo
from os import unlink
from PIL import Image
from subprocess import DEVNULL
from subprocess import PIPE
from subprocess import Popen
def do_it(cmd, fifo_path):
mkfifo(fifo_path)
# I hear you like subprocesses with your subprocesses...
sub_proc = Popen(cmd, stdout=DEVNULL, stderr=PIPE, shell=True)
pillow_image = Image.open(fifo_path)
proc_exit = sub_proc.wait()
unlink(fifo_path)
fifo_path = '/tmp/myfifo.bmp'
cmd = '/usr/bin/convert -resize 100 /path/to/some/bad_or_missing.tif ' + fifo_path
proc = Process(target=do_it, args=(cmd, fifo_path))
proc.daemon = True
proc.start()
proc.join(timeout=3) # I can set a timeout here
# Seems heavy anyway, and how do I get pillow_image back for further work?
pillow_image.show()
希望这些能够说明我的问题以及我所尝试的内容。提前谢谢。
答案 0 :(得分:2)
尝试从空管道或FIFO读取时:
如果没有进程打开管道进行写入,则read()将返回0 表示文件结束。
Image.open(fifo_path)
可能会卡住,当且仅当命令在打开fifo_path
时才会因为写入而被阻止。
Normally, opening the FIFO blocks until the other end is opened also.
这是一个正常的序列:
cmd
阻止尝试打开 fifo_open
进行写作 cmd
关闭管道末端。您的代码收到EOF,因为没有其他进程打开FIFO进行写入并且Image.open(fifo_path)
返回。
管道的cmd
末端由于成功完成或由于错误而关闭,无论cmd
是否突然被杀是无关紧要的:只要它结束了。
您的流程是否会调用proc.wait()
并不重要。 proc.wait()
不会杀死cmd
。 proc.wait()
不会阻止管道的另一端被打开或关闭。 proc.wait()
唯一要做的就是等到子进程终止和/或返回已经死的子进程的退出状态。
这是死锁案例:
Image.open()
来电时,cmd
甚至没有尝试打开fifo_open
因为任何原因写作,例如,没有/usr/bin/convert
,错误的命令 - 行参数,错误/无输入等 fifo_open
未针对撰写而打开,因此Image.open(fifo_open)
永远无法尝试打开阅读。
您可以打开FIFO以便在后台线程中写入,并在父级打开FIFO进行读取时将其关闭:
#!/usr/bin/env python3
import contextlib
import os
import subprocess
import sys
import textwrap
import threading
fifo_path = "fifo"
with contextlib.ExitStack() as stack:
os.mkfifo(fifo_path)
stack.callback(os.remove, fifo_path)
child = stack.enter_context(
subprocess.Popen([
sys.executable, '-c', textwrap.dedent('''
import random
import sys
import time
if random.random() < 0.5: # 50%
open(sys.argv[1], 'w').write("ok")
else:
sys.exit("fifo is not opened for writing in the child")
'''), fifo_path
]))
stack.callback(child.kill)
opened = threading.Event() # set when the FIFO is opened for reading
threading.Thread(target=open_for_writing, args=[fifo_path, opened, child],
daemon=True).start()
pipe = stack.enter_context(open(fifo_path)) # open for reading
opened.set() # the background thread may close its end of the pipe now
print(pipe.read()) # read data from the child or return in 3 seconds
sys.exit(child.returncode)
在EOF上,孩子被杀了。
open_for_writing()
打开FIFO时,取消阻止open(fifo_path)
,然后启用关闭它。为避免pipe.read()
过早返回,它会让孩子3秒钟打开FIFO进行写入:
def open_for_writing(path, opened, child):
with open(path, 'w'):
opened.wait() # don't close until opened for reading in the main thread
try:
child.wait(timeout=3) # the child has 3 seconds to open for writing
except subprocess.TimeoutExpired:
pass
如果您确定子进程要么尝试打开FIFO或最终退出(或者您可以在子进程运行时挂起Python进程,那么您可以放弃超时并使用child.wait()
代替child.wait(timeout=3)
。通过该更改,没有任意超时,代码可能在任意慢的系统上工作(无论出于何种原因)。
代码演示了为什么应该尽可能避免线程,或者为什么人们应该更喜欢已建立的模式(不太通用但保证正常工作),例如通过通信进行同步。
答案中的代码应该适用于各种情况,但这些部分错综复杂。在一个非常具体的案例实现之前,即使是一个小的改变也可能不会显现出来。