阿里妹导读:做为在平常开发生产中很是实用的语言,有必要掌握一些python用法,好比爬虫、网络请求等场景,非常实用。但python是单线程的,如何提升python的处理速度,是一个很重要的问题,这个问题的一个关键技术,叫协程。本篇文章,讲讲python协程的理解与使用,主要是针对网络请求这个模块作一个梳理,但愿能帮到有须要的同窗。
在理解协程这个概念及其做用场景前,先要了解几个基本的关于操做系统的概念,主要是进程、线程、同步、异步、阻塞、非阻塞,了解这几个概念,不只是对协程这个场景,诸如消息队列、缓存等,都有必定的帮助。接下来,编者就本身的理解和网上查询的材料,作一个总结。node
在面试的时候,咱们都会记住一个概念,进程是系统资源分配的最小单位。是的,系统由一个个程序,也就是进程组成的,通常状况下,分为文本区域、数据区域和堆栈区域。python
文本区域存储处理器执行的代码(机器码),一般来讲,这是一个只读区域,防止运行的程序被意外修改。c++
数据区域存储全部的变量和动态分配的内存,又细分为初始化的数据区(全部初始化的全局、静态、常量,以及外部变量)和为初始化的数据区(初始化为0的全局变量和静态变量),初始化的变量最初保存在文本区,程序启动后被拷贝到初始化的数据区。面试
堆栈区域存储着活动过程调用的指令和本地变量,在地址空间里,栈区紧连着堆区,他们的增加方向相反,内存是线性的,因此咱们代码放在低地址的地方,由低向高增加,栈区大小不可预测,随开随用,所以放在高地址的地方,由高向低增加。当堆和栈指针重合的时候,意味着内存耗尽,形成内存溢出。编程
进程的建立和销毁都是相对于系统资源,很是消耗资源,是一种比较昂贵的操做。进程为了自身能获得运行,必需要抢占式的争夺CPU。对于单核CPU来讲,在同一时间只能执行一个进程的代码,因此在单核CPU上实现多进程,是经过CPU快速的切换不一样进程,看上去就像是多个进程在同时进行。json
因为进程间是隔离的,各自拥有本身的内存内存资源,相比于线程的共同共享内存来讲,相对安全,不一样进程之间的数据只能经过 IPC(Inter-Process Communication) 进行通讯共享。segmentfault
线程是CPU调度的最小单位。若是进程是一个容器,线程就是运行在容器里面的程序,线程是属于进程的,同个进程的多个线程共享进程的内存地址空间。缓存
线程间的通讯能够直接经过全局变量进行通讯,因此相对来讲,线程间通讯是不太安全的,所以引入了各类锁的场景,不在这里阐述。安全
当一个线程崩溃了,会致使整个进程也崩溃了,即其余线程也挂了, 但多进程而不会,一个进程挂了,另外一个进程依然照样运行。服务器
在多核操做系统中,默认进程内只有一个线程,因此对多进程的处理就像是一个进程一个核心。
同步和异步关注的是消息通讯机制,所谓同步,就是在发出一个函数调用时,在没有获得结果以前,该调用不会返回。一旦调用返回,就当即获得执行的返回值,即调用者主动等待调用结果。
所谓异步,就是在请求发出去后,这个调用就当即返回,没有返回结果,经过回调等方式告知该调用的实际结果。同步的请求,须要主动读写数据,而且等待结果;异步的请求,调用者不会马上获得结果。而是在调用发出后,被调用者经过状态、通知来通知调用者,或经过回调函数处理这个调用。
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用是指调用结果返回以前,当前线程会被挂起。调用线程只有在获得结果以后才会返回。非阻塞调用指在不能马上获得结果以前,该调用不会阻塞当前线程。因此,区分的条件在于,进程/线程要访问的数据是否就绪,进程/线程是否须要等待。
非阻塞通常经过多路复用实现,多路复用有 select、poll、epoll几种实现方式。
在了解前面的几个概念后,咱们再来看协程的概念。
协程是属于线程的,又称微线程,纤程,英文名Coroutine。举个例子,在执行函数A时,我但愿随时中断去执行函数B,而后中断B的执行,切换回来执行A。这就是协程的做用,由调用者自由切换。这个切换过程并非等同于函数调用,由于它没有调用语句。执行方式与多线程相似,可是协程只有一个线程执行。
协程的优势是执行效率很是高,由于协程的切换由程序自身控制,不须要切换线程,即没有切换线程的开销。同时,因为只有一个线程,不存在冲突问题,不须要依赖锁(加锁与释放锁存在不少资源消耗)。
协程主要的使用场景在于处理IO密集型程序,解决效率问题,不适用于CPU密集型程序的处理。然而实际场景中这两种场景很是多,若是要充分发挥CPU利用率,能够结合多进程+协程的方式。后续咱们会讲到结合点。
根据wikipedia的定义,协程是一个无优先级的子程序调度组件,容许子程序在特色的地方挂起恢复。因此理论上,只要内存足够,一个线程中能够有任意多个协程,但同一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。协程是为了充分发挥异步调用的优点,异步操做则是为了不IO操做阻塞线程。
在了解原理前,咱们先作一个知识的准备工做。
1)现代主流的操做系统几乎都是分时操做系统,即一台计算机采用时间片轮转的方式为多个用户服务,系统资源分配的基本单位是进程,CPU调度的基本单位是线程。
2)运行时内存空间分为变量区,栈区,堆区。内存地址分配上,堆区从低地到高,栈区从高往低。
3)计算机执行时一条条指令读取执行,执行到当前指令时,下一条指令的地址在指令寄存器的IP中,ESP寄存值指向当前栈顶地址,EBP指向当前活动栈帧的基地址。
4)系统发生函数调用时操做为:先将入参从右往左依次压栈,而后把返回地址压栈,最后将当前EBP寄存器的值压栈,修改ESP寄存器的值,在栈区分配当前函数局部变量所需的空间。
5)协程的上下文包含属于当前协程的栈区和寄存器里面存放的值。
在python3.3中,经过关键字yield from使用协程,在3.5中,引入了关于协程的语法糖async和await,咱们主要看async/await的原理解析。其中,事件循环是一个核心所在,编写过 js的同窗,会对事件循环Eventloop更加了解, 事件循环是一种等待程序分配事件或消息的编程架构(维基百科)。在python中,asyncio.coroutine 修饰器用来标记做为协程的函数, 这里的协程是和asyncio及其事件循环一块儿使用的,而在后续的发展中,async/await被使用的愈来愈普遍。
async/await是使用python协程的关键,从结构上来看,asyncio 实质上是一个异步框架,async/await 是为异步框架提供的 API已方便使用者调用,因此使用者要想使用async/await 编写协程代码,目前必须机遇 asyncio 或其余异步库。
在实际开发编写异步代码时,为了不太多的回调方法致使的回调地狱,但又须要获取异步调用的返回结果结果,聪明的语言设计者设计了一个 叫Future的对象,封装了与loop 的交互行为。其大体执行过程为:程序启动后,经过add_done_callback 方法向 epoll 注册回调函数,当 result 属性获得返回值后,主动运行以前注册的回调函数,向上传递给 coroutine。这个Future对象为asyncio.Future。
可是,要想取得返回值,程序必须恢复恢复工做状态,而因为Future 对象自己的生存周期比较短,每一次注册回调、产生事件、触发回调过程后工做可能已经完成,因此用 Future 向生成器 send result 并不合适。因此这里又引入一个新的对象 Task,保存在Future 对象中,对生成器协程进行状态管理。
Python 里另外一个 Future 对象是 concurrent.futures.Future,与 asyncio.Future 互不兼容,容易产生混淆。区别点在于,concurrent.futures 是线程级的 Future 对象,当使用 concurrent.futures.Executor 进行多线程编程时,该对象用于在不一样的 thread 之间传递结果。
上文中提到,Task是维护生成器协程状态处理执行逻辑的的任务对象,Task 中有一个_step 方法,负责生成器协程与 EventLoop 交互过程的状态迁移,整个过程能够理解为:Task向协程 send 一个值,恢复其工做状态。当协程运行到断点后,获得新的Future对象,再处理 future 与 loop 的回调注册过程。
在平常开发中,会有一个误区,认为每一个线程均可以有一个独立的 loop。实际运行时,主线程才能经过 asyncio.get_event_loop() 建立一个新的 loop,而在其余线程时,使用 get_event_loop() 却会抛错。正确的作法为经过 asyncio.set_event_loop() ,将当前线程与 主线程的loop 显式绑定。
Loop有一个很大的缺陷,就是 loop 的运行状态不受 Python 代码控制,因此在业务处理中,没法稳定的将协程拓展到多线程中运行。
介绍完概念和原理,我来看看如何使用,这里,举一个实际场景的例子,来看看如何使用python的协程。
外部接收一些文件,每一个文件里有一组数据,其中,这组数据须要经过http的方式,发向第三方平台,并得到结果。
因为同一个文件的每一组数据没有先后的处理逻辑,在以前经过Requests库发送的网络请求,串行执行,下一组数据的发送须要等待上一组数据的返回,显得整个文件的处理时间长,这种请求方式,彻底能够由协程来实现。
为了更方便的配合协程发请求,咱们使用aiohttp库来代替requests库,关于aiohttp,这里不作过多剖析,仅作下简单介绍。
aiohttp是asyncio和Python的异步HTTP客户端/服务器,因为是异步的,常常用在服务区端接收请求,和客户端爬虫应用,发起异步请求,这里咱们主要用来发请求。
aiohttp支持客户端和HTTP服务器,能够实现单线程并发IO操做,无需使用Callback Hell便可支持Server WebSockets和Client WebSockets,且具备中间件。
直接上代码了,talk is cheap, show me the code~
import aiohttp import asyncio from inspect import isfunction import time import logger @logging_utils.exception(logger) def request(pool, data_list): loop = asyncio.get_event_loop() loop.run_until_complete(exec(pool, data_list)) async def exec(pool, data_list): tasks = [] sem = asyncio.Semaphore(pool) for item in data_list: tasks.append( control_sem(sem, item.get("method", "GET"), item.get("url"), item.get("data"), item.get("headers"), item.get("callback"))) await asyncio.wait(tasks) async def control_sem(sem, method, url, data, headers, callback): async with sem: count = 0 flag = False while not flag and count < 4: flag = await fetch(method, url, data, headers, callback) count = count + 1 print("flag:{},count:{}".format(flag, count)) if count == 4 and not flag: raise Exception('EAS service not responding after 4 times of retry.') async def fetch(method, url, data, headers, callback): async with aiohttp.request(method, url=url, data=data, headers=headers) as resp: try: json = await resp.read() print(json) if resp.status != 200: return False if isfunction(callback): callback(json) return True except Exception as e: print(e)
这里,咱们封装了对外发送批量请求的request方法,接收一次性发送的数据多少,和数据综合,在外部使用时,只须要构建好网络请求对象的数据,设定好请求池大小便可,同时,设置了重试功能,进行了4次重试,防止在网络抖动的时候,单个数据的网络请求发送失败。
在使用协程重构网络请求模块以后,当数据量在1000的时候,由以前的816s,提高到424s,快了一倍,且请求池大小加大的时候,效果更明显,因为第三方平台同时创建链接的数据限制,咱们设定了40的阀值。能够看到,优化的程度很显著。
人生苦短,我用python。协程好很差,谁用谁知道。若是有相似的场景,能够考虑启用,或者其余场景,欢迎留言讨论。
参考资料:
理解async/await:
https://segmentfault.com/a/11...
协程概念,原理(c++和node.js实现)
https://cnodejs.org/topic/58d...
本文做者: 墨辨
本文来自阿里云合做伙伴“阿里技术”,如需转载请联系原做者。