使用线程/进程在Django中运行长时间异步任务

33
免责声明:我知道 SO 上有几个类似的问题。我认为我读了大部分,如果不是全部,但没有找到答案(请参阅后面的真正问题)。我还知道使用 celery 或其他异步队列系统是实现长时间运行任务的最佳方法——或者至少使用 cron 管理的脚本。还有 mod_wsgi doc about processes and threads 但我不确定我的理解是否正确。
问题是:
使用下面列出的解决方案时涉及到的确切风险/问题是什么?它们中的任何一个适合长时间运行任务吗(好的,即使 celery 更适合)?我的问题实际上更多地是了解 wsgi 和 python/django 的内部工作方式,而不是寻找最佳的整体解决方案。线程阻塞、对变量的不安全访问、僵尸进程等问题。
假设:
1. 我的"long_process"正在执行某些非常安全的操作。即使它失败了,我也不在乎。 2. Python >=2.6 3. 我正在使用带有 Apache 的 mod_wsgi(使用守护进程模式),那么,如果使用 uwsgi 或 gunicorn 会发生什么变化吗?
mod_wsgi配置:
WSGIDaemonProcess NAME user=www-data group=www-data threads=25
WSGIScriptAlias / /path/to/wsgi.py
WSGIProcessGroup %{ENV:VHOST}

我认为以下是可用于启动独立进程(广义上的意思)来执行长时间运行任务并快速向用户返回响应的选项:

os.fork

import os

if os.fork()==0:
    long_process()
else:
    return HttpResponse()

子进程

import subprocess

p = subprocess.Popen([sys.executable, '/path/to/script.py'], 
                                    stdout=subprocess.PIPE, 
                                    stderr=subprocess.STDOUT)

(脚本可能是 manage.py 命令的地方)

线程

import threading

t = threading.Thread(target=long_process,
                             args=args,
                             kwargs=kwargs)
t.setDaemon(True)
t.start()
return HttpResponse()

NB.

表示“注意”或者“重要提示”。
该段文字讲解了在CPython中由于全局解释器锁(Global Interpreter Lock),只能有一个线程执行Python代码,但是一些性能优化的库可能会克服这个限制。如果想要应用程序更好地利用多核机器的计算资源,建议使用multiprocessing。然而,如果想要同时运行多个I/O密集型任务,仍然可以使用线程模型。
问题描述主线程将快速返回(httpresponse)。产生的长线程是否会阻塞WSGI做其他请求?

multiprocessing

表示“多进程”。
from multiprocessing import Process

p = Process(target=_bulk_action,args=(action,objs))
p.start()
return HttpResponse()

这应该解决线程并发问题,不是吗?
这些是我能想到的选项。哪些会起作用,哪些不会,为什么?

这个讨论非常有趣,可以补充一些特别针对Django的信息:http://groups.google.com/group/django-developers/browse_thread/thread/905f79e350525c95 - Stefano
3个回答

30

os.fork

fork会克隆父进程,这里是您的Django stack。既然你只想运行一个单独的Python脚本,使用它似乎是不必要的冗余。

subprocess

使用subprocess预计是交互式的。换句话说,虽然您可以使用此方法有效地生成进程,但预期您会在完成后终止它。如果您将一个留在运行中,Python可能会为您清理,但我猜这实际上会导致内存泄漏。

threading

线程是定义的逻辑单元。当调用其run()方法时,它们开始运行,并在run()方法执行结束时终止。这使它们非常适合创建将在当前范围之外运行的逻辑分支。然而,正如您所提到的,它们受全局解释器锁的影响。

multiprocessing

这个模块允许您生成进程,并具有与threading类似的API。你可以说这就像类似于线程的类固醇。这些进程不受全局解释器锁的影响,可以利用多核架构。但是,由于此原因,它们更难处理。

因此,您的选择实际上只有线程或进程。如果您可以使用线程并且它对您的应用程序有意义,请使用线程。否则,请使用进程。


1
谢谢Chris。你能详细解释一下线程->“受GDI限制”和进程->“更复杂,因此难以处理”的意思吗?_线程_:比如说,如果有两个不同的Web请求生成了两个不同的线程来处理不同的资源,它们能够并发运行吗?我猜应该不行(特别是如果来自单进程mod_wsgi)。但根据Graham的说法,它们不会阻塞其他请求的服务,这仍然是最重要的事情。_进程_:困难是否与使它们通信有关,还是还有其他原因?谢谢! - Stefano
1
全局解释器锁只与CPython实现有关。Python解释器的这个特定版本强制限制一次只能有一个线程能够使用字节码。这可能不是你需要担心的问题,但在互操作性方面,这是需要注意的事情。 - Chris Pratt
2
线程几乎是非常容易处理的。你只需要继承Thread,定义__init__run方法就可以开始了。多进程处理涉及到工作池的概念。初始化更加复杂,需要仔细管理正在进行的任务。这也是为什么许多现代应用程序仍然不实际利用多个核心的原因。你可以使用所有的处理能力做出惊人的东西,但这就像指挥交响乐一样--你需要付出一些努力才能达到完美的效果。 - Chris Pratt

11
我发现使用uWSGI Decorators比使用Celery更简单,如果您只需要在后台运行一些长时间的任务。对于严肃的大型项目,Celery是最好的解决方案,但对于执行简单操作来说,它会增加额外负担。
要开始使用uWSGI Decorators,您只需要更新uWSGI配置即可。
<spooler-processes>1</spooler-processes>
<spooler>/here/the/path/to/dir</spooler>

写出如下代码:
@spoolraw
def long_task(arguments):
    try:
        doing something with arguments['myarg'])
    except Exception as e:
        ...something...
    return uwsgi.SPOOL_OK

def myView(request)
    long_task.spool({'myarg': str(someVar)})
    return render_to_response('done.html')

当您开始在uWSGI日志中查看时,会出现以下内容:
[spooler] written 208 bytes to file /here/the/path/to/dir/uwsgi_spoolfile_on_hostname_31139_2_0_1359694428_441414

任务完成时:

[spooler /here/the/path/to/dir pid: 31138] done with task uwsgi_spoolfile_on_hostname_31139_2_0_1359694428_441414 after 78 seconds

我发现有一些(对我来说)奇怪的限制:

    - spool can receive as argument only dictionary of strings, look like because it's serialize in file as strings.
    - spool should be created on start up so "spooled" code it should be contained in separate file which should be defined in uWSGI config as <import>pyFileWithSpooledCode</import>

不知道uWSGI装饰器! - Stefano
在运行了 pip install uwsgi 之后,我仍然无法在 Python shell 中导入 uwsgi。有什么建议吗?@Oleg Neumyvakin - YeRuizhi

4
针对这个问题:
“生成的长线程会阻塞wsgi不执行其他请求吗?”
答案是否定的。
但是,在创建后台线程时,您仍然需要小心。如果您创建了大量线程并堵塞整个进程,那么您真的需要一个任务排队系统,即使您正在进行进程内处理。
在Web进程中进行分叉或执行操作,特别是从Apache中,通常不是一个好主意,因为Apache可能会对所创建的子进程的环境施加奇怪的条件,从技术上讲,这可能会干扰其操作。
使用像Celery这样的系统仍然可能是最佳解决方案。

谢谢Graham。希望能得到mod_wsgi创建者对这个问题的反馈。只是为了确认:关于GDI/阻塞线程,我如何配置WSGIDaemonProcess (processes=P) threads=T是否有任何区别? - Stefano

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