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复用,运行网络服务器和客户端
这里面有一些假设,我将分析一下:
人们需要单线程并发代码
人们经常需要I/O复用
人们使用协同程序
首先,我们真的需要单线程并发执行代码吗?
在过去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 实现异步任务
使用celery的常见场景:
Web应用。当用户触发一个动作需要较长时间来执行完成时,可以把它作为任务交给celery异步执行,执行完再返回给用户。这点和你在前端使用ajax实现异步加载有异曲同工之妙。
定时任务。假设有多台服务器,多个任务,定时任务的管理是很困难的,你要在不同电脑上写不同的crontab,而且还不好管理。Celery可以帮助我们快速在不同的机器设定不同任务。
其他可以异步执行的任务。比如发送短信,邮件,推送消息,清理/设置缓存等。这点还是比较有用的。
综上所述,第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异步执行任务的程序写完了。除此之外,还可以写很多的异步任务,发邮件就是非常典型的一种。
Celery如何修复Python的GIL问题的更多相关文章
- [转载] Python的GIL是什么鬼,多线程性能究竟如何
原文: http://cenalulu.github.io/python/gil-in-python/ GIL是什么 首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器( ...
- 使用进程池规避Python的GIL限制
操作系统 : CentOS7.3.1611_x64 python版本:2.7.5 问题描述 Python的GIL会对CPU密集型的程序产生影响,如果完全使用Python来编程,怎么避开GIL的限制呢? ...
- Python的GIL机制与多线程编程
GIL 全称global interpreter lock 全局解释锁 gil使得python同一个时刻只有一个线程在一个cpu上执行字节码,并且无法将多个线程映射到多个cpu上,即不能发挥多个cpu ...
- 004_浅析Python的GIL和线程安全
在这里我们将介绍Python的GIL和线程安全,希望大家能从中理解Python里的GIL,以及GIL的前世今生. 对于Python的GIL和线程安全很多人不是很了解,通过本文,希望能让大家对Pytho ...
- Python中GIL
GIL(global interpreter lock)全局解释器锁 python中GIL使得同一个时刻只有一个线程在一个cpu上执行,无法将多个线程映射到多个cpu上执行,但GIL并不会一直占有,它 ...
- Python GIL 系列之再谈Python的GIL
1. 之前写过一篇<通过实例认识Python的GIL>的文章,感觉有些意犹未尽 2. 这次对例子作了些扩展,进一步的分析GIL对Python程序的影响 2.1 先来看例子: [python ...
- Python的GIL是什么鬼,多线程性能究竟如何
前言:博主在刚接触Python的时候时常听到GIL这个词,并且发现这个词经常和Python无法高效的实现多线程划上等号.本着不光要知其然,还要知其所以然的研究态度,博主搜集了各方面的资料,花了一周内几 ...
- 我大概知道他在说什么了,是对内存单元的竞争访问吧。Python有GIL,在执行伪码时是原子的。但是伪码之间不保证原子性。 UDP丢包,你是不是做了盲发?没有拥塞控制的情况下,确实会出现丢包严重的情况。你先看看发送速率,还有是否带有拥塞控制。
我大概知道他在说什么了,是对内存单元的竞争访问吧.Python有GIL,在执行伪码时是原子的.但是伪码之间不保证原子性. UDP丢包,你是不是做了盲发?没有拥塞控制的情况下,确实会出现丢包严重的情 ...
- python基础--GIL全局解释器锁、Event事件、信号量、死锁、递归锁
ps:python解释器有很多种,最常见的就是C python解释器 GIL全局解释器锁: GIL本质上是一把互斥锁:将并发变成串行,牺牲效率保证了数据的安全 用来阻止同一个进程下的多个线程的同时执行 ...
随机推荐
- ubuntu16搭建LAMP环境
准备工作: 安装ubuntu16虚拟机,可以正常访问网络 更新为国内源(下载快一些) 1.安装apache sudo apt-get install apache2 然后打开我们的浏览器,访问一下 1 ...
- lvs+keepalived集群架构服务
一,LVS功能详解 1.1 LVS(Linux Virtual Server)介绍 LVS是Linux Virtual Server 的简写(也叫做IPVS),意即Linux虚拟服务器,是一个虚拟的服 ...
- 为LPC1549 LPCXpresso评估板开发基于mbed的项目
本文将主要介绍如何使用Visual Studio和VisualGDB为LPC1549 LPCXpresso开发板创建一个使用mbed框架的基础项目. LPC1549 LPCXpresso开发板载一个L ...
- 使用poi进行数据的导出Demo
这是本人在项目中遇到了一个导出数据时,如果该条数据中包含汉字,就会出现excel单元格的大小与期望的样式不一样,也是查找了半天,也没有发现哪里出的问题. 现将一个小Demo奉献在这里,可以在遇到使用p ...
- 搭建jenkins+python+selenium+robot framework环境
1.安装jenkins 具体参考:https://www.cnblogs.com/dydxw/p/10538103.html 2.下载插件 我是为了方便,把有关python.selenium.robo ...
- Linux下干净卸载mysql
1.首先查看mysql的安装情况 rpm -qa|grep -i mysql 显示之前安装了: MySQL-client-5.5.25a-1.rhel5 MySQL-server-5.5.25a-1. ...
- FreeMarker生成word
FreeMarker生成word数据填充是通过,Map填充. Map dataMap = new HashMap<String, Object>(); List<User> l ...
- 2.3 vue配置(上)
rm,在打包之前把上一次打包之后的东西删掉,然后webpack重新打包 通过DefinePlugin形成一个环境变量 HTML打包插件
- oracle 备份恢复之recover database的四条语句区别
1 recover database using backup controlfile2 recover database until cancel3 recover database usin ...
- Cogs 1714. [POJ1741][男人八题]树上的点对(点分治)
[POJ1741][男人八题]树上的点对 ★★★ 输入文件:poj1741_tree.in 输出文件:poj1741_tree.out 简单对比 时间限制:1 s 内存限制:256 MB [题目描述] ...