【Python入门】49.异步IO之 协程

摘要:介绍什么是异步IO,什么是协程。在Python中是如何通过Generator实现协程运行的。


*写在前面:为了更好的学习python,博主记录下自己的学习路程。本学习笔记基于廖雪峰的Python教程,如有侵权,请告知删除。欢迎与博主一起学习Pythonヽ( ̄▽ ̄)ノ *


本学习笔记基于廖雪峰的Python教程。欢迎与博主一起学习Pythonヽ( ̄▽ ̄)ノ
本节内容:介绍什么是异步IO,什么是协程。在Python中是如何通过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

整个流程由一个线程执行,produceconsumer协作完成任务,这种方式便是协程。

子程序就是协程的一种特例。——Donald Knuth


以上就是本节的全部内容,感谢你的阅读。

下一节内容:异步IO之 asyncio

有任何问题与想法,欢迎评论与吐槽。

和博主一起学习Python吧( ̄▽ ̄)~*

猜你喜欢

转载自blog.csdn.net/lecorn/article/details/82807460