协程在手,说走就走
什么是协程
先介绍–生产者-消费者模式
生产者消费者模式并不是GOF提出的23种设计模式之一,23种设计模式都是建立在面向对象的基础之上的,但其实面向过程的编程中也有很多高效的编程模式,生产者消费者模式便是其中之一,它是我们编程过程中最常用的一种设计模式。
在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield
跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高
def consumer():
# 消费者
r = ''
while True:
n = yield r # 将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() # 类似于iter(range(10)),即
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
生产者每生成一个数,消费者接收到并返回接受状态,细看代码,也就用到了个yield
和send
,但这正是python
实现协程的核心。
首先分析一下代码的执行
生产者首先c.send(None)
,启动消费者生成器,一旦启动生成器,消费者那边的yield
将r
发送到消费者这边,是个空的字符串。
一旦消费者yield
,程序就跑到生产者这边了,消费者就暂停执行,也就是说n = yield r
只执行了yield r
,n
都没有赋值的。
然后消费者这边开始执行while
循环,n
自增,然后就执行r=send(n)
,这边的send
是带参数的,这个参数会发送到消费者那边,给上次的n
进行赋值,也就是说生产者通过send
将消费者从暂停状态激活了。
那么消费者的n
赋值为1后,就继续执行消费者的程序了,打印并且将r
进行赋值,然后又到了yield r
,这就将重新赋值的r
又发送给了生产者的r
。
。。。
就这样一直迭代着,生产者给生产n
,消费者发给生产者r
。
看懂了吗?这到底说了个啥?为什么要这样干?都把人绕晕了…
消费者反而是个生成器,真有意思。
这个模型的核心在于利用yield
实现了函数间的中断,也就是协程的核心。
把上面两个函数看成是两个普通函数,也就是说一个两个函数能够随时中断去执行另外一个函数,这就很神奇了。
定义协程
python提供了async
去定义一个协程函数,await
进行中断挂起去执行其他协程。
import asyncio
import time
async def main():
print('hello')
await asyncio.sleep(1)
print('world')
start = time.time()
asyncio.run(main())
print(f'cost time: {time.time() - start}')
输出
hello
[暂停了一秒]
world
cost time: 1.0051383972167969
定义一个协程就是这么简单,可是这也看不出协程的优点啊,一毫秒的时间也没有节省,那是因为这只有一个协程,也不用切换到其他协程。
那么使用多个协程看一下效果
import asyncio
import time
async def main():
print('hello')
await asyncio.sleep(1)
print('world')
start = time.time()
asyncio.run(asyncio.wait([main(), main()]))
print(f'cost time: {time.time() - start}')
这里使用了两个协程,看一下效果
hello
hello
world
world
cost time: 1.0056376457214355
时间几乎一样,如果不用协程,执行函数肯定最少要2s,但现在只要1s,极大缩短了运行时间。
那么协程和多线程哪个更快呢?有人做了实验进行了对比爬虫协程比线程爬取速度更快? - 北风之神0509 - 博客园 (cnblogs.com)
应用
协程应用比较多的还是IO操作,包括网络IO,文件操作IO,将所有IO操作都进行await
,这样可以极大缩短运行时间。
写代码的时候建议多进程和协程结合使用。