Celery如何修复Python的GIL问题

小结:

1、

 

Celery如何修复Python的GIL问题
https://python.freelycode.com/contribution/detail/346

 

最近,我重读了Glyph写的Unyielding。如果你还没有读过,那赶紧去。我将会在下文略述它的内容,但是,原文绝对值得一读。

近十年我都在研究Python全局解释器锁,即GIL。

关于GIL,真正的问题是异步I/O--线程就是作为处理它的简洁方法推广的。你接收到一个请求,你创建一个线程,魔法发生了。关注是分开的而资源是共享的。

但是在Python里,你不能高效的这样做,因为线程需要争夺GIL,而每个解释器只有一个GIL,无论你的机器有多少核。所以,即使你使用顶配的英特尔酷睿i7处理器,你也不会觉得使用线程使性能有很大提升。

理论上是这样的,现实可能更糟糕--Python 3.1之前的GIL实际上在多核处理器上处理多线程时性能更糟糕并且可能使你的代码变得更慢。

 

异步I/O是问题吗?

 

现代编程中我们做的大多数任务都可以归结为I/O,或者通常这样回答:

例如,从数据库取数是I/O--你等待数据的时候,系统可以同时做其他事,比如,服务更多请求。

asyncio最近向Python添加的内容是

 

    使用协同程序(coroutines)编写单线程并发代码,通过socket和其他资源实现I/O复用,运行网络服务器和客户端

 

这里面有一些假设,我将分析一下:

  1. 人们需要单线程并发代码

  2. 人们经常需要I/O复用

  3. 人们使用协同程序

首先,我们真的需要单线程并发执行代码吗?

在过去10年里,我从来没有遇见一个人指出“这个代码需要并发执行但是使用单线程。”

我认为这里的意思其实是我们需要并发执行,这是GIL最具争议的地方--我们不能实现真正的并发。

我最近意识到我们真的不需要并发--稍后讨论这点,此前,我们来列出后面这两条假设,扔掉协同程序。

人们使用协同程序吗?是的,但是不用在生产环境中。这可能有点武断,但是我使用很多种语言编写并发程序,并且从来没有遇到过像协同程序那么难读的。

如果代码意味着可读,那么协同程序就意味着弄瞎你的眼睛。

另外,协作并发又叫协同程序在很久之前就被抛弃了。为什么?

因为协作程序的最基本假设是他们合作。抢先并发强制,或者至少尝试强制,公平使用资源。

协同程序并没有这样幸运--如果你的协同程序阻塞了,你必须等待。你的线程等待所有其他的协同程序。

那是协同程序在现实中的最大问题。如果你的程序是一个shell脚本,用于计算斐波那契数,那或许还行。但是在这种困境,服务器断掉,连接超时,我们不能阅读读取任何安装的开源库的代码。

回到并发执行--我限制可以使用的线程数了吗?不,一次也没有。

我觉得我们既不需要协同程序,也不需要并发性。

我认为我们需要的,并且值得花费精力研究的是,非阻塞代码。

 

阻塞代码的问题

 

代码是阻塞的是说代码具有以下两个特征之一:

  •   真得阻塞

  •   完成需要很长时间

人类是没有耐心的,但是在等待机器完成操作时我们看起来更急不可耐。长时间等待,还是避免阻塞,其实是同一个问题。

我们不需要阻塞一些可以很快做完的事,比如,等待一些慢的操作(数据库请求)完成时,可以响应用户。

抢先并发(线程)是解决这个问题的好方法,有一些优点:

  •    代码可读性好

  •    更好的利用资源 

对于线程可读性--不多说了,他们不是最简单的可以理解的,但是绝对比协同程序好。

关于资源--那真得是从“更好一点”到“不可置信”转变的实现细节。他们中的大部分可以使用双核机器中的两个核。

在经典线程编程我们可以:

如果thread_procession_data超时--我们将会得到一个错误。当第二个核可用时它会使用第二个核。漂亮。

现在我们也可以用Python这样做--不完成一样,但是接近。我们可以把执行处理数据的代码放进一个进程里而不使用thread_procession_data。我当然是在说超级棒的multiprocession库。

但是,那样真的更好吗?

我仍然需要理解几个概念,尤其是进程间是如何共享资源的,那看起来不是很明显。

有更好的方法吗?

 

无锁的胜利和Celery

 

作为程序员我只想要不阻塞的代码。

我不关心是通过进程,线程,事物内存还是魔法实现的。

创建一个工作单元,描述它的参数,比如优先级,你的工作完成了。在Python世界里有一个包可以满足你--Celery。

Celery是一个庞大的项目,开始你的第一个任务前有繁杂的配置。但是一旦它开始工作,就变得美妙。

举个例子,工作中我有一个系统,在各种各样的网络入口拉取一个社会股。调用API需要时间,还需要加上网络连接收发数据的时间。

例如:

 

面向用户的代码超过一秒钟都不能被接受。然而我需要仅在用户访问记录视图时才触发这段代码。我该怎么做?

有了Celery,我就可以用一个任务(task)包裹update_metrics,然后这样做:

这里:

  •   update_metrics 是一个耗时操作,但是并没有阻塞

  •   queue参数指定执行任务的队列

update_metrics耗时很长--但是多亏Celery我不需要考虑那些:    

  •     由用户动作准确触发

  •     代码可读性高,并且非常明确

  •     资源可用时便会使用

最重要的是:我不必再苦恼于代码是否在执行I/O,我是否应该让出,或者它是被CPU或I/O强迫的。

 

Celery可以做的事

 

你的问题有:抓取1000 URLs,然后计算用户在表格里指定的3个词的频率。

通常,这很难--你需要定位将要抓取的URLs。连接可能超时,你需要一直等待直到所有任务完成,并且你需要以某种方式存储用户的输入。

不使用Celery,搞清楚哪里以及如何存储数据就是一个噩梦。使用Celery我只需要任务:

这个例子的主要部分在最后几行:

  •   chain用来在任务间建造通道,一个的输出成为另一个的输入

  •   chord用来将任务分组,使一个任务在其他任务都完成后才执行

这样写优点有很多:

  •     你不需要了解它是如何执行的。可以是线程或进程或协同程序。(在某种程度上,Celery支持所有类型池)

  •     你不???

对了,这个例子里也有一些缺点:

  •     因为Celery任务是函数,我们不得不使用scrape_url.subtask(args=(url,))语法,它并不易读

  •    Celery需要明确的任务路径,作为内嵌函数的任务,通常,task.py模块--不能在其他任务中定义或者提交任务

  •     因为我们不能在一个任务的内部定义或者通过调用另一个任务串联起任务,需要chord和chain这样的对象,而这些对象使代码变复杂

 

无锁?

 

抛开前面列出的问题,对我而言,最大的问题是细粒度控制的缺乏。队列是一个伟大的实现无锁编程的基本模型。

前面的例子假设你需要执行一堆任务然后聚合结果--大约30行代码的map/reduce。

但是让我们考虑一种更加困难的情形--假设我们有一个不支持并发的任务,完全是无锁的,但是需要读写而不使用阻塞。

我们应该怎么做?

首先(这里我假设你使用Django整合)

这就是运行一个同时处理最多一个任务的任务执行单元(worker)所需要做的全部工作。

 

 

 

因此,不用做任何特别的事情,没有锁,没有GIL问题,我们可以读写一个值。

当然,这有一个主要问题--我们不能同时读取,即使可以。

 

结语

 

对我来讲,这总结了整个GIL和协同程序/同步的争论。我认为Python核心的主要问题是它很大程度上由C启发。但是,在这里我认为这是Python的缺陷。

而且我不知道这方面努力的原因。

有数百家公司运行Python代码作为连接逻辑(glue logic)--单线程同步代码(看看Django多受欢迎)但是这些公司服务数以万计的用户。

我认为如果我们想要Python完全支持同步,这是方法。引入基于队列的完全无锁以及允许编程人员修改队列。

Celery已经实现了其中的大部分。为了在Python里拥有这些,我们需要扩展解释器来管理任务执行单元和队列,添加一些语法糖衣用来进行内嵌任务的定义和调用以便使用。

作为编程人员,我认为我们从来不需要异步代码。我从来不需要协同程序,也从来不需要多重I/O。

我需要的是高效表达想法和我想到它的方式的工具,抽象线程、抛弃同步、使用无锁例程解决了这个问题。

 

 

https://mp.weixin.qq.com/s/rmOwqwLRoGPR40486xL8dg

Django 使用 Celery 实现异步任务

Python开发者 2017-08-24

使用celery的常见场景:

 

  1. Web应用。当用户触发一个动作需要较长时间来执行完成时,可以把它作为任务交给celery异步执行,执行完再返回给用户。这点和你在前端使用ajax实现异步加载有异曲同工之妙。

  2. 定时任务。假设有多台服务器,多个任务,定时任务的管理是很困难的,你要在不同电脑上写不同的crontab,而且还不好管理。Celery可以帮助我们快速在不同的机器设定不同任务。

  3. 其他可以异步执行的任务。比如发送短信,邮件,推送消息,清理/设置缓存等。这点还是比较有用的。

 

综上所述,第1点和第3点的用途是我考虑celery的原因。目前,考虑在Django中实现两个功能:

 

  • 文章阅读量的统计

  • 发送邮件

 

关于文章阅读量的统计,我之前的做法就是在用户每一次访问文章的时候,都会同步执行一遍+1的函数,现在打算用异步执行的方式。

 

下面介绍在Django中的使用方法:

 

1、环境准备

 

安装celery,rabbitmq,django-celery.

 

2、启动消息中间件rabbitmq。

 

用它的原因是celery官方推荐的就是它,也可以用Redis等,但Redis会因为断电的原因造成数据全部丢失等问题。

 

让其在后台运行:

 

sudo rabbitmq-server -detached

 

3、在Django中配置(源代码)

 

项目代码结构

 

dailyblog

 

    ├── blog

   ├── models.py

   ├── serializer.py

   ├── tasks.py

   ├── urls.py

   ├── views.py

├── config.yaml

├── dailyblog

   ├── celery.py

   ├── __init__.py

   ├── __init__.pyc

   ├── settings.py

   ├── urls.py

   ├── wsgi.py

 

对于celery的配置,需要编写几个文件:

 

  1、dailyblog/celery.py

 

  2、dailyblog/settings.py

 

  3、blog/tasks.py

 

  4、dailyblog/__init__.py

 

1、dailyblog/celery.py

 

本模块主要是创建了celery应用,配置来自django的settings文件。

 

from __future__ import absolute_import,unicode_literals #目的是拒绝隐士引入,celery.py和celery冲突。

import os

from celery import Celery

from django.conf import settings

 

 

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dailyblog.settings")

 

#创建celery应用

app = Celery('dailyblog')

#You can pass the object directly here, but using a string is better since then the worker doesn’t have to serialize the object.

app.config_from_object('django.conf:settings')

#如果在工程的应用中创建了tasks.py模块,那么Celery应用就会自动去检索创建的任务。比如你添加了一个任务,在django中会实时地检索出来。

app.autodiscover_tasks(lambda :settings.INSTALLED_APPS)

 

关于config_from_object,我对于如何加载配置文件还是比较感兴趣的,于是研究了一下源码,具体可以见:“celery加载配置文件”。

 

2、settings.py

 

配置celery,

 

import djcelery

djcelery.setup_loader()

 

 

#末尾添加

CELERYBEAT_SCHEDULER = ‘djcelery.schedulers.DatabaseScheduler‘  # 这是使用了django-celery默认的数据库调度模型,任务执行周期都被存在你指定的orm数据库中

 

#INstalled_apps

     INSTALLED_APPS = (

    ‘django.contrib.admin‘,

    ‘django.contrib.auth‘,

    ‘django.contrib.contenttypes‘,

    ‘django.contrib.sessions‘,

    ‘django.contrib.messages‘,

    ‘django.contrib.staticfiles‘,

    ‘djcelery‘,    #### 这里增加了djcelery 也就是为了在django admin里面可一直接配置和查看celery

    ‘blog‘,     ###

)

 

setup_loader目的是设定celery的加载器,源码:

 

  def setup_loader():  # noqa

    os.environ.setdefault(

        b'CELERY_LOADER', b'djcelery.loaders.DjangoLoader',

    )

 

3、dailyblog/init.py

 

from __future__ import absolute_import

 

# This will make sure the app is always imported when

# Django starts so that shared_task will use this app.

from .celery import app as celery_app

 

4、blog/tasks.py

 

from django.db.models import F

 

from .models import Article

from dailyblog import celery_app

 

 

@celery_app.task

def incr_readtimes(article_id):

    return Article.objects.filter(id=article_id).update(read_times=F('read_times') + 1)

 

这里面添加了一个任务。任务可以通过delay方法执行,也可以周期性地执行。

 

这里还需要注意,如果把上面任务的返回值赋值给一个变量,那么程序也会被阻塞,需要等待异步任务返回的结果。因此,实际应用不需要赋值。

 

上面的代码写好后,要执行数据库更新:

 

python manage.py makemigrations

python manage.py migrate.

 

Django会创建了几个数据库,分别为:

 

Crontabs Intervals Periodic tasks Tasks Workers

 

在views.py添加异步任务:

 

from .tasks import incr_readtimes

class ArticleDetailView(BaseMixin,DetailView):

 

 

    def get(self, request, *args, **kwargs):

        .......

         incr_readtimes.delay(self.object.id)

 

这里不需要赋值。

 

下面要启动celery,我采用supervisor进程管理器来管理celery:

 

[program:celery]

command= celery -A dailyblog worker --loglevel=INFO

directory=/srv/dailyblog/www/

numprocess=1

startsecs=0

stopwaitsecs=0

autostart=true

autorestart=true

stdout_logfile=/tmp/celery.log

stderr_logfile=/tmp/celery.err

 

重新加载supervisor.conf文件,然后启动celery:

 

supervisorctl start celery

 

至此,通过celery异步执行任务的程序写完了。除此之外,还可以写很多的异步任务,发邮件就是非常典型的一种。

 
 
 
 
 
posted @ 2019-05-21 22:55  papering  阅读(362)  评论(1编辑  收藏  举报