防止从空FIFO中读取数据阻塞

时间:2016-11-01 01:13:46

标签: python python-3.x subprocess

在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()

希望这些能够说明我的问题以及我所尝试的内容。提前谢谢。

1 个答案:

答案 0 :(得分:2)

POSIX read(2)

  

尝试从空管道或FIFO读取时:

     

如果没有进程打开管道进行写入,则read()将返回0   表示文件结束。

Image.open(fifo_path)可能会卡住,当且仅当命令在打开fifo_path时才会因为写入而被阻止

  

Normally, opening the FIFO blocks until the other end is opened also.

这是一个正常的序列:

  1. cmd阻止尝试打开 fifo_open进行写作
  2. 尝试打开进行阅读时,您的Python代码块
  3. 一旦两个进程打开FIFO,数据流就会启动。除了名称之外,FIFO类似于管道 - 只有一个管道对象 - 内核在内部传递所有数据 而不将其写入文件系统。 {{ 3}}
  4. cmd关闭管道末端。您的代码收到EOF,因为没有其他进程打开FIFO进行写入并且Image.open(fifo_path)返回。

    管道的cmd末端由于成功完成或由于错误而关闭,无论cmd是否突然被杀是无关紧要的:只要它结束了。

    您的流程是否会调用proc.wait()并不重要。 proc.wait()不会杀死cmdproc.wait()不会阻止管道的另一端被打开或关闭。 proc.wait()唯一要做的就是等到子进程终止和/或返回已经死的子进程的退出状态。

  5. 这是死锁案例:

    1. Image.open()来电时,cmd甚至没有尝试打开fifo_open因为任何原因写作,例如,没有/usr/bin/convert,错误的命令 - 行参数,错误/无输入等
    2. 尝试打开进行阅读时,您的Python代码块
    3. 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)。通过该更改,没有任意超时,代码可能在任意慢的系统上工作(无论出于何种原因)。

      代码演示了为什么应该尽可能避免线程,或者为什么人们应该更喜欢已建立的模式(不太通用但保证正常工作),例如通过通信进行同步。

      答案中的代码应该适用于各种情况,但这些部分错综复杂。在一个非常具体的案例实现之前,即使是一个小的改变也可能不会显现出来。