前言:python由于GIL(全局锁)的存在,不能发挥多核的优势,其性能一直饱受诟病。然而在IO密集型的网络编程里,异步处理比同步处理能提升成百上千倍的效率,弥补了python性能方面的短板,如最新的微服务框架japronto,resquests per second可达百万级。
python还有一个优势是库(第三方库)极为丰富,运用十分方便。asyncio是python3.4版本引入到标准库。python3.5又加入了async/await特性。
概念
在学习asyncio之前,我们先来理清楚 同步/异步的概念 :
- 同步是指完成事务的逻辑,先执行第一个事务,如果阻塞了,会一直等待,直到这个事务完成,再执行第二个事务,顺序执行
- 异步是和同步相对的,异步是指在处理调用这个事务的之后,不会等待这个事务的处理结果,直接处理第二个事务去了,通过状态、通知、回调来通知调用者处理结果。
为了尽可能的发挥出cpu的性能,使程序运行的更有效率,软件跑的更快,可以通过多种方法来实现,比如多进程、多线程或者异步等等。
先对比一下这几个概念:进程,线程、协程。
进程
进程(process)是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是系统进行资源分配和调度的基本单位。我们日常使用电脑和手机打开的应用程序就属于进程 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-83NQWnJq-1601297530192)(assets/1582016546488.png)]
进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
进程出现的原因
进程的出现是为了更充分的利用CPU的资源使得能够并发执行任务。假设有两个任务,一个是IO操作,一个是计算任务。如果一个任务接一个任务的执行,CPU在执行IO操作时需要等待数据读取完关闭文件后才能执行下一个计算任务,会使得CPU得不到充分的利用。如果在等待数据读取时让空闲的CPU切换到另一个任务去执行,等数据读取完后再切换回去就可以使CPU得到更充分的利用。要做到任务切换,需要能够识别任务以及保存和恢复任务的状态,而且不同的任务通常需要不同的系统资源,为此进程就被发明出来。系统正是通过进程分配资源、标识任务的。
进程的优缺点:
优点:
- 相对比较稳定安全(进程拥有独立的系统资源,进程间不容易相互影响)
缺点:
- 进程切换的时空开销比较大(涉及到很多系统资源的切换)
- 进程间通信(IPC)较为复杂和耗时
线程
线程(thread)是在进程之后发展出来的概念。线程包含在进程中,也叫轻量级进程。线程是进程中一个单一顺序的控制流,像“线”一样(或许是其译名的由来),它是系统进行运算调度(即如何分配CPU去执行不同任务)的基本单位,一个进程的多个线程在执行不同任务的同时共享进程的系统资源(如虚拟地址空间,文件描述符等),如果把进程比作一个正在生产产品的车间,那么线程就好比是车间里面执行不同任务的工人,也可把进程比作道路,而线程就好比是并行的车道。线程由相关堆栈寄存器和线程控制块组成。
线程出现的原因
线程的出现是为了减少任务切换的消耗,提高系统的并发性,实现让一个进程也能执行多个任务。例如一个文本程序需要获取键盘输入、显示文本内容并将文本内容保存到磁盘。如果使用多个进程来执行这些任务,需要频繁的进行上下文切换和进程间通信。考虑到这些任务是相互关联且共享资源的(它们都要用到文本内容),用一个进程中的多个线程来执行可以减少上下文切换和进程间通信的消耗。
线程的优缺点
优点:
- 线程切换的开销比进程切换的开销小,减少了任务切换的消耗,提高了操作系统的并发性能。
缺点:
- 相比进程不够稳定,多线程在操作共享数据时容易出错(比如丢失数据、产生死锁)
协程
协程是一种用户态的轻量级线程,又称"微线程",英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。
子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。
协程的起始处是第一个入口点,在协程里,返回点之后是接下来的入口点。
在python中,协程可以通过yield来调用其它协程。通过yield方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的,通过相互协作共同完成任务。其运行的大致流程如下:
第一步,协程A开始执行。
第二步,协程A执行到一半,进入暂停,通过yield命令将执行权转移到协程B。
第三步,(一段时间后)协程B交还执行权。
第四步,协程A恢复执行。
协程的特点
协程的特点在于是一个线程执行,与多线程相比,其优势体现在:
协程的执行效率非常高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显,在处理大规模并发连接(IO密集型任务)时,协程要优于线程。
协程不需要多线程的锁机制。在协程中控制共享资源不加锁,只需要判断状态就好了。
Tips:利用多核CPU最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
yield
从生成器到协程
为了弄清楚 yield 和 return 的区别,我们写两个没有什么用途的函数:
def foo():
for i in range(5):
yield i
g = foo()
print(next(g))
print(next(g))
print(g.send(None))
生成器为我们引入了暂停函数执行(yield
)的功能。当有了暂停的功能之后,人们就想能不能在生成器暂停的时候向其发送一点东西(其实上面也有提及:send(None)
)。这种向暂停的生成器发送信息的功能通过 PEP 342
进入 Python 2.5
中,并催生了 Python
中协程
的诞生。根据 wikipedia
中的定义 协程是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序。
协程和线程,有相似点,多个协程之间和线程一样,只会交叉串行执行;也有不同点
,线程之间要频繁进行切换,加锁,解锁,从复杂度和效率来看,和协程相比,这确是一个痛点。协程通过使用 yield
暂停生成器,可以将程序的执行流程交给其他的子程序,从而实现不同子程序的之间的交替执行。
下面通过一个演示来看看,如何向生成器中发送消息。
def foo():
for i in range(5):
result = yield i
print('result', result)
g = foo()
print(next(g))
print(next(g))
print(g.send(1))
print(g.send(2))
print(g.send(3))
案例:协程计算平均值
向一个生成器中不断传入新的数字,计算所有数字的平均值
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count
coro_avg = averager()
next(coro_avg)
print(coro_avg.send(10))
print(coro_avg.send(30))
print(coro_avg.send(5))
yield form
yield from
是在Python3.3才出现的语法。所以这个特性在Python2中是没有的。
yield from
后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。
yield from后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰。
# 子生成器
def average():
total = 0
count = 0
average = 0
while True:
new_num = yield average
count += 1
total += new_num
average = total / count
# 委托生成器
def proxy_gen():
while True:
yield from average()
# 调用方
def main():
calc_average = proxy_gen()
next(calc_average) # 预激下生成器
print(calc_average.send(10)) # 打印:10.0
print(calc_average.send(20)) # 打印:15.0
print(calc_average.send(30)) # 打印:20.0
if __name__ == '__main__':
main()
实现生成器的嵌套,并不是一定必须要使用yield from
,而是使用yield from
可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现。
如果自己用yield
去实现,那只会加大代码的编写难度,降低开发效率,降低代码的可读性。既然Python已经想得这么周到,我们当然要好好利用起来。
讲解它之前,首先要知道这个几个概念
调用方
:调用委派生成器的客户端(调用方)代码委托生成器
:包含yield from表达式的生成器函数子生成器
:yield from后面加的生成器函数
你可能不知道他们都是什么意思,没关系,来看下这个例子。
这个例子,是实现实时计算平均值的。
比如,第一次传入10,那返回平均数自然是10.
第二次传入20,那返回平均数是(10+20)/2=15
第三次传入30,那返回平均数(10+20+30)/3=20
委托生成器的作用是:在调用方与子生成器之间建立一个双向通道
。
调用方可以通过send()
直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。
你可能会经常看到有些代码,还可以在yield from
前面看到可以赋值。这是什么用法?
你可能会以为,子生成器 yield 回来的值,被委托生成器给拦截了。你可以亲自写个demo运行试验一下,并不是你想的那样。
因为我们之前说了,委托生成器,只起一个桥梁作用,它建立的是一个双向通道
,它并没有权利也没有办法,对子生成器yield回来的内容做拦截。
asyncio
asyncio的几个概念
在了解 asyncio
的使用方法前,首先有必要先介绍一下,这几个贯穿始终的概念。
event_loop 事件循环
:程序开启一个无限的循环,程序员会把一些函数(协程)注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。coroutine 协程
:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。future 对象
: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别task 任务
:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。Task 对象是 Future 的子类,它将 corou