在线程中使用Popen会阻止每个传入的Flask-SocketIO请求

时间:2016-01-04 20:49:22

标签: python multithreading flask pipe popen

我有以下情况: 我在socketio服务器上收到了请求。我回答它(socket.emit(..))和然后在另一个线程中重启计算负载

如果繁重的计算是由subprocess.Popen引起的(使用subprocess.PIPE),只要它被执行,它就完全阻止每个传入的请求,尽管它发生在一个单独的线程中。

没问题 - 在this thread中,建议异步读取子进程的结果,缓冲区大小为1,以便在这些读取之间,其他线程有机会执行某些操作。不幸的是,这对我没有帮助。

我也已经monkeypatched eventlet并且工作正常 - 只要我不在线程中使用subprocess.Popensubprocess.PIPE

在此代码示例中,您可以看到只有subprocess.Popensubprocess.PIPE一起使用才会发生。取消注释#functionWithSimulatedHeavyLoad()而改为评论functionWithHeavyLoad()时,一切都像魅力一样。

from flask import Flask
from flask.ext.socketio import SocketIO, emit
import eventlet

eventlet.monkey_patch()
app = Flask(__name__)
socketio = SocketIO(app)

import time
from threading  import Thread

@socketio.on('client command')
def response(data, type = None, nonce = None):
    socketio.emit('client response', ['foo'])
    thread = Thread(target = testThreadFunction)
    thread.daemon = True
    thread.start()

def testThreadFunction():
    #functionWithSimulatedHeavyLoad()
    functionWithHeavyLoad()

def functionWithSimulatedHeavyLoad():
    time.sleep(5)

def functionWithHeavyLoad():
    from datetime import datetime
    import subprocess
    import sys
    from queue import Queue, Empty

    ON_POSIX = 'posix' in sys.builtin_module_names

    def enqueueOutput(out, queue):
        for line in iter(out.readline, b''):
            if line == '':
                break
            queue.put(line)
        out.close()

    # just anything that takes long to be computed
    shellCommand = 'find / test'

    p = subprocess.Popen(shellCommand, universal_newlines=True, shell=True, stdout=subprocess.PIPE, bufsize=1, close_fds=ON_POSIX)
    q = Queue()
    t = Thread(target = enqueueOutput, args = (p.stdout, q))
    t.daemon = True
    t.start()
    t.join()

    text = ''

    while True:
        try:
            line = q.get_nowait()
            text += line
            print(line)
        except Empty:
            break

    socketio.emit('client response', {'text': text})

socketio.run(app)

客户收到消息' foo'在functionWithHeavyLoad()函数完成后阻塞工作。不过,它应该早些收到消息。

此示例可以复制并粘贴到.py文件中,并且可以立即重现该行为。

我使用的是Python 3.4.3,Flask 0.10.1,flask-socketio1.2,eventlet 0.17.4

更新

如果我将它放入functionWithHeavyLoad函数中它实际上有效并且一切都很好:

import shlex
shellCommand = shlex.split('find / test')

popen = subprocess.Popen(shellCommand, stdout=subprocess.PIPE)

lines_iterator = iter(popen.stdout.readline, b"")
for line in lines_iterator:
    print(line)
    eventlet.sleep()

问题是:我使用find进行重载,以便让您的样本更容易重现。但是,在我的代码中,我实际上使用tesseract "{0}" stdout -l deu作为sell命令。这(与find不同)仍会阻止一切。这是一个问题而不是eventlet的tesseract问题吗?但是仍然如此:如果它在一个单独的线程中发生,当find没有阻塞时,它会逐行读取上下文切换,这怎么会阻塞?

1 个答案:

答案 0 :(得分:10)

感谢这个问题,我今天学到了新东西。 Eventlet提供了一个greenlet友好版本的子进程及其功能,但由于一些奇怪的原因,它不会在标准库中修补此模块。

链接到子进程的eventlet实现:https://github.com/eventlet/eventlet/blob/master/eventlet/green/subprocess.py

查看eventlet patcher,修补的模块是os,select,socket,thread,time,MySQLdb,builtins和psycopg2。修补程序中绝对没有对子进程的引用。

好消息是,在我更换后,我能够在与你的应用程序非常相似的应用程序中使用Popen()

import subprocess

使用:

from eventlet.green import subprocess

但请注意,当前发布的eventlet版本(0.17.4)不支持universal_newlines中的Popen选项,如果使用它将会出错。支持此选项的是master(这是添加选项的commit)。您将不得不从调用中删除该选项,或者直接从github安装eventlet的主分支。