`eventlet.spawn`未按预期工作。

3
我正在编写一个用于数据分析任务的Web UI。
以下是其工作方式:
用户指定参数,如数据集学习率后,我会创建一个新的任务记录,然后异步启动执行器来运行此任务(执行器可能需要很长时间才能完成),并将用户重定向到其他页面。
在寻找适用于Python的异步库后,我选择了eventlet,以下是我在flask视图函数中编写的代码:
db.save(task)
eventlet.spawn(executor, task)
return redirect("/show_tasks")

使用上述代码,执行器根本没有执行。
我的代码可能有什么问题?或者我应该尝试其他方法吗?
3个回答

8

虽然已经给出了直接的解决方案,但我会尝试回答你的第一个问题并解释为什么你的代码没有按预期工作。

声明:我目前维护Eventlet。为了适应合理的大小,本评论将包含许多简化。

协同式多线程的简要介绍

有两种方式实现多线程,而Eventlet则利用协作式方法。其核心是Greenlet库,它基本上允许您创建独立的“执行上下文”。我们可以将这样的上下文视为所有本地变量的冻结状态和下一条指令的指针。基本上,多线程=上下文+调度程序。Greenlet提供上下文,因此我们需要一个调度程序,即使得决定哪个上下文应该占用CPU的东西。为了做出决策,我们还需要运行一些代码。这意味着我们需要另一个上下文(绿色线程)。在Eventlet代码库中,这个特殊的绿色线程被称为Hub。调度程序维护一个有序 set 上下文集合,这些上下文需要尽快运行-运行队列和等待某些事情完成的上下文集合(例如,网络IO或时间限制睡眠)。

但由于我们正在进行合作式多任务处理,除非显式地将上下文切换到另一个上下文,否则一个上下文将无限期地执行。这是一种非常悲哀的编程风格,并且根据定义与现有库不兼容(指他们知道谁);因此,Eventlet提供了常见模块的绿色版本,以这种方式更改,使它们转换为Hub而不是阻塞所有内容。然后,一些时间可能会花费在其他绿色线程或Hub的等待外部事件实现中,在这种情况下,Hub会切换回起始该事件的绿色线程 - 并且它将继续执行。

完毕。现在回到你的问题。


eventlet.spawn 实际上是做什么的:它创建一个新的执行上下文,基本上就是在内存中分配一个对象。同时,它告诉调度程序将此上下文放入运行队列中,以便在第一个可能的时刻,Hub 将切换到新生成的函数。您的代码没有提供这样的时刻。没有地方明确地放弃执行权给其他绿色线程,对于 Eventlet,通常是通过 eventlet.sleep() 实现的。而且,由于您没有使用常见模块的绿色版本,因此在等待其他代码时隐式地放弃执行权的机会也不存在。最合适(如果不是唯一)的地方应该是您的 WSGI 服务器的接受循环:在等待下一个请求时,它应该给其他绿色线程运行的机会。在第一个答案中提到的 eventlet.monkey_patch() 只是用其相应的绿色版本替换所有(或子集)常见模块的一种便捷方式。



整体设计上不需要的意见 在单独的部分中,可以轻松跳过。如果您正在构建防错误软件,通常希望限制生成的线程(包括但不限于“green”)和进程的执行时间,并至少报告(记录)或响应其未处理的错误。在提供的代码中,生成的绿色线程可能在下一时刻或五分钟后(再次因为没有人放弃CPU)运行或失败并出现未处理的异常。幸运的是,Eventlet提供了两个解决方案:Timeout with_timeout() 允许限制等待时间(请记住,如果它不放弃,您无法限制它),GreenThread.link() 用于捕获所有异常。对于我来说,重新引发“主”代码中的异常很诱人,link() 可以轻松实现,但请考虑异常将从睡眠和IO调用中引发 - 这些是您放弃到Hub的地方。这可能会提供一些非常反直觉的回溯。


4
你需要打补丁一些系统库才能使eventlet正常工作。这里是一个最小化的工作示例(也可查看gist):
#!/usr/bin/env python 

from flask import Flask 
import time 
import eventlet 

eventlet.monkey_patch() 

app = Flask(__name__) 
app.debug = True 

def background(): 
    """ do something in the background """ 
    print('[background] working in the background...') 
    time.sleep(2) 
    print('[background] done.') 
    return 42 

def callback(gt, *args, **kwargs): 
    """ this function is called when results are available """ 
    result = gt.wait() 
    print("[cb] %s" % result) 

@app.route('/') 
def index(): 
    greenth = eventlet.spawn(background) 
    greenth.link(callback) 
    return "Hello World" 

if __name__ == '__main__': 
    app.run() 

更多相关内容:

“编写像Eventlet这样的库的一个挑战是,内置的网络库不支持我们需要的协作式yielding。”

函数在“monkey_patch”之后被调用,但直到我“SIGINT”服务器进程才返回。有任何想法为什么会发生这种情况吗?我在函数中调用了一些“mapreduce”客户端。 - satoru
我认为我使用的“客户端”出了问题。我可以通过在“IPython”中调用执行器来重现此问题。 - satoru
@Satoru.Logic,我更新了我的 Gist,加入了一个结果回调的例子。 - miku
对我不起作用: Traceback: File "x.py", line 36, in <module> app.run() File "/usr/lib/python2.7/site-packages/flask/app.py", line 772, in run run_simple(host, port, self, **options) File "/usr/lib/python2.7/site-packages/werkzeug/serving.py", line 622, in run_simple reloader_type) File "/usr/lib/python2.7/site-packages/werkzeug/_reloader.py", line 265, in run_with_reloader reloader.run() File "/usr/lib/python2.7/site-packages/werkzeug/_reloader.py", line 167, in run self._sleep(self.interval) TypeError: sleep()最多只能接受1个参数(给出了2个) - MarSoft

2
Eventlet可能确实适合您的目的,但它并不适用于任何旧应用程序; Eventlet要求它控制您的应用程序的所有I/O。
您可以尝试以下两种方式之一:
1. 在另一个线程中启动Eventlet的主循环,或者 2. 不使用Eventlet,只需在另一个线程中生成您的任务。 Celery可能是另一个选项。

谢谢解答我的问题。我对这种async场景不太熟悉,使用Thread.new和类似eventlet.spawn之间有什么区别? - satoru
@Satoru.Logic:eventlet.spawn会创建一个新的Eventlet任务。Eventlet使用协程实现协作式多任务处理。要想让你的任务得到执行,必须确保Eventlet的调度器正在运行。在正常的Eventlet应用中,它可以控制应用程序并按需进行调度。但是,在不受Eventlet调度器控制的应用程序中,Eventlet永远不会有机会安排你的任务。 - icktoofay
@Satoru.Logic:另一方面,线程通常由操作系统支持(通常是抢占式调度)。 - icktoofay
我该如何启动调度程序呢?当我尝试将 eventlet.monkey_patch() 添加到我的 flask 应用中时,执行器被调用。这个 patching 和调度程序有什么关系吗? - satoru
@Satoru.Logic:无论如何,我不知道你的任务是CPU绑定还是I/O绑定,但如果是CPU绑定,你可能需要使用线程。Eventlet适用于I/O。 - icktoofay
显示剩余2条评论

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