摘要:介绍什么是异步IO,什么是协程。在Python中是如何通过Generator实现协程运行的。
*写在前面:为了更好的学习python,博主记录下自己的学习路程。本学习笔记基于廖雪峰的Python教程,如有侵权,请告知删除。欢迎与博主一起学习Pythonヽ( ̄▽ ̄)ノ *
本学习笔记基于廖雪峰的Python教程。欢迎与博主一起学习Pythonヽ( ̄▽ ̄)ノ
本节内容:介绍什么是异步IO,什么是协程。在Python中是如何通过Generator实现协程运行的。
目录
异步IO
协程
Generator的send()函数
Generator实现协程
异步IO
在IO编程一节中,我们知道CPU的速度远远快于磁盘、网络等IO。在一个线程中,遇到IO操作时,CPU往往需要等待IO操作完成后才能执行下一步操作,这种情况称为同步IO。
为了加快代码的运行速度,我们可以使用多进程或多线程来并发执行代码,解决一个线程被阻塞而影响其他代码运行的问题。
但是系统运行线程的数量也是有限的,而且当线程数量过多,CPU忙于切换线程而非执行代码,运行效率大大降低。
要解决这个问题,就要用到异步IO。
当代码需要执行一个IO操作时,只发出IO指令,让磁盘去执行,而CPU不等待iO结果,继续执行其他代码。一段时间后,当IO返回结果时,再通知CPU进行处理。这种情况称为异步IO。
在同步IO的情况下,遇到IO操作,主线程被挂起,阻塞了其他代码的运行;
在异步IO的情况下,遇到IO操作,一个线程可以处理多个多个IO请求,大大提高系统的多任务处理能力。
协程
在学习异步IO之前,我们需要了解一个重要的概念——协程。
协程(Coroutine),又称微线程,纤程。
我们回顾一下进程与线程。
进程:我们每打开一个程序就是打开一个进程,比如一个浏览器,一个游戏等;
线程:在一个进程中,会包含多个线程,比如在浏览器中,我们可以看视频,听音乐等。
在同步机制下,一个线程就是执行一个子程序,或者我们称之为函数。子程序的调用只有一个入口,一次返回,且调用顺序是明确的。
而协程与上面所说的线程不一样。
协程看上去也是子程序,但执行过程中,在一个子程序可中断,然后去执行另一个子程序(不是函数调用),在适当的时候再返回来接着执行。
看个简单的示例:
def A():
print(1)
print(2)
print(3)
def B():
print('a')
print('b')
print('c')
现在有两个子程序A和B,如果由线程来执行,执行A打印123
,执行B打印abc
。如果由协程来执行,在执行A的过程中,可中断然后去执行B,然后再回来执行A,打印结果可能是这样的:
1
2
a
b
3
c
这看起来像是多线程,但实际上只有一个线程,这便是协程的特点。
这个特点使得协程有两大优势,第一,不存在切换线程的开销,子程序的切换靠程序自身的控制;第二,不需要多线程的锁机制,因为只有一个线程。这使得协程的效率比多线程高很多。
所以,用多进程+协程的方式,多核CPU的充分利用加上协程的高效率,使得系统运行获得极高的性能。
Generator的send()函数
在Python中,通过生成器generator来实现协程。
还记得生成器generator吗?
generator是一种一边循环一边计算的机制,它保存的是一种算法,可以通过next()函数来调用并返回计算结果。
def A():
n = 0
while True:
n = n + 1
print('Start')
yield n
print('End')
a = A()
现在a是一个generator,我们可以通过不断调用next()函数来返回计算结果n的值:
>>>next(a)
Start
1
>>>next(a)
End
Start
2
>>>next(a)
End
Start
3
注意到,第一次调用next()时,代码执行到第一个yield(包含yield),下一次调用next(),会接着从上一次yield语句后面的代码开始执行。
了解了next()函数的执行过程后,我们再来看send()函数。send()与next()类似,但多了一个赋值的功能。
我们先把yield n
改为x = yield n
,再添加print(x)
语句:
def A():
n = 0
while True:
n = n + 1
print('Start')
x = yield n
print(x)
print('End')
a = A()
这个时候我们依然可以调用next(),代码不会出错:
>>>next(a)
Start
1
>>>next(a)
None
End
Start
2
注意到这里多输出了一个None
,说明yield n
实际上是一个表达式,而它的值为None
。
send()函数的作用就是可以给yield n
赋值,像这样:
>>>a.send(None)
Start
1
>>>a.send(100)
100
End
Start
2
执行send()
函数时,是先给yield n
赋值,然后执行yield
后面的语句。这里把100赋给了变量x
。
由于第一次调用send()
函数时没有可以赋值的对象,所以必须使用send(None)
。send(None)
的作用与next()
是一样的。
Generator实现协程
我们通过Generator的send()
函数来不断切换子程序,从而实现协程的运行。
来看一个生产者—消费者模型的例子 (例子源自廖雪峰官网):
传统的做法是用一个线程来生产信息,一个线程来获取信息,通过锁机制控制队列和等待,但一不小心就可能死锁。
改用协程,生产者在生产信息后,通过yield
跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
执行结果:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
代码解析:
函数consumer()
是一个生成器,把consumer
传入函数produce()
执行:
首先在produce
中,调用c.send(None)
启动生成器consumer
。
在produce
生产信息之后,通过调用r = c.send(n)
,向consumer
发送信息n
,并且返回consumer
的消费情况r
。
当n=5
时,produce
停止生产,调用c.close()
关闭生成器consumer
。
整个流程由一个线程执行,produce
和consumer
协作完成任务,这种方式便是协程。
子程序就是协程的一种特例。——Donald Knuth
以上就是本节的全部内容,感谢你的阅读。
下一节内容:异步IO之
有任何问题与想法,欢迎评论与吐槽。
和博主一起学习Python吧( ̄▽ ̄)~*