上篇文章我们说过由于GIL锁的限制,导致Python不能充分利用多线程来实现高并发,在某些情况下使用多线程可能比单线程效率更低,所以Python中出现了协程。
协程(coroutine) 又称微线程,是一中轻量级的线程,它可以在函数的特定位置暂停或恢复,同时调用者可以从协程中获取状态或将状态传递给协程。进程和线程都是通过CPU的调度实现不同任务的有序执行,而协程是由用户程序自己控制调度的,也没有线程切换的开销,所以执行效率极高。
早先的协程是使用生成器关键字yield来实现的,和生成器很相似。下面利用一生产者-消费者模型来介绍如何使用yield协程来进行任务的切换。
import time
def consumer():
r = None
while True:
n = yield r
if not n:
return
print("[消费者] 消费'{}'...".format(n))
time.sleep(1)
r = '消费完成'
def producer(c):
next(c)
n = 0
while n < 5:
n = n + 1
print("[生产者] 生产'{}'...".format(n))
res = c.send(n)
print("[生产者] 消费者返回'{}'".format(res))
c.close()
if __name__ == '__main__':
c = consumer()
producer(c)
执行结果如下:
上述代码中的consumer函数是一个生成器,
这段代码全程在一个线程中完成,producer和consumer协作完成任务,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。
如果有上百个任务,要想实现在多个任务之间切换,使用yield生成器的方式就过于麻烦,而greenlet模块可以很轻易的实现。
Greenlet是Python的⼀个C扩展,旨在提供可⾃⾏调度的"微线程",也就是协程。在greenlet模块中,通过target.switch()可以切换到指定的协程,可以更简单的进行切换任务。
可以使用命令pip install greenlet
安装greenlet模块。
from greenlet import greenlet
import time
def work1():
while True:
print("work1开始执行...")
g2.switch() # 切换到g2中运行
time.sleep(0.5)
def work2():
while True:
print("work2开始执行...")
g1.switch() # 切换到g1中运行
time.sleep(0.5)
if __name__ == "__main__":
# 定义greenlet对象
g1 = greenlet(work1)
g2 = greenlet(work2)
g1.switch() # 切换到g1中运行
执行结果就是work1和work2交替执行。
虽然greenlet模块实现了协程并且可以方便的切换任务,但是仍需要人工切换,而不是自动进行任务的切换,当一个任务执行时如果遇到IO(⽐如⽹络、⽂件操作等),就会阻塞,没有解决遇到IO自动切换来提升效率的问题。
其实Python还有⼀个⽐greenlet更强⼤的协程模块gevent,gevent也是基于greenlet的,可以实现任务的自动切换,当⼀个greenlet遇到IO操作时,就会⾃动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执⾏。这样就跳过了IO操作的时间,⽽不是等待IO完成,可以提升程序的运行效率。
可以使用命令pip install gevent
安装gevent模块。
import gevent
import time
def work1():
for i in range(5):
print("work1开始执行...", gevent.getcurrent())
time.sleep(0.5)
def work2():
for i in range(5):
print("work2开始执行...", gevent.getcurrent())
time.sleep(0.5)
if __name__ == "__main__":
g1 = gevent.spawn(work1)
g2 = gevent.spawn(work2)
# 等待协程执⾏完成再关闭主线程
g1.join()
g2.join()
执行结果如下:
我们希望的是gevent模块帮我们⾃动切换协程,以达到work1和work2交替执⾏的⽬的,但并没有达到效果,原因是因为我们使用time.sleep(0.5)来模拟IO耗时操作,但是这样并没有被gevent正确识别为IO操作,所以要使⽤下⾯的gvent.sleep()来实现耗时
import gevent
import time
def work1():
for i in range(5):
print("work1开始执行...", gevent.getcurrent())
gevent.sleep(0.5)
def work2():
for i in range(5):
print("work2开始执行...", gevent.getcurrent())
gevent.sleep(0.5)
if __name__ == "__main__":
g1 = gevent.spawn(work1)
g2 = gevent.spawn(work2)
# 等待协程执⾏完成再关闭主线程
g1.join()
g2.join()
运行结果如下:
上面把time.sleep()改写成gevent.sleep()后,work1和work2能够交替执⾏,那么有没有不用改写,就可以实现的方法吗?答案是有的,那就是给程序打猴子补丁
。
关于猴⼦补丁:这个叫法起源于Zope框架,⼤家在修正Zope的Bug的时候经常在程序后⾯追加更新部分,这些被称作是"杂牌军补丁"(guerilla patch),后来guerilla就渐渐的写成了gorllia(猩猩),再后来就写成了monkey(猴⼦),所以现在被称为猴⼦补丁。
猴⼦补丁主要有以下⼏个⽤处:
可以使用以下代码给程序打猴子补丁:
import gevent
# 打补丁,让gevent识别⾃⼰提供或者⽹络请求的耗时操作
from gevent import monkey
monkey.patch_all()
import time
def work1():
for i in range(5):
print("work1开始执行...", gevent.getcurrent())
time.sleep(0.5)
def work2():
for i in range(5):
print("work2开始执行...", gevent.getcurrent())
time.sleep(0.5)
if __name__ == "__main__":
g1 = gevent.spawn(work1)
g2 = gevent.spawn(work2)
# 等待协程执⾏完成再关闭主线程
g1.join()
g2.join()
执行结果如下:
可以看出给程序打上猴子补丁后,使用time.sleep(),gevent也能识别到,可以自动切换任务。
在python2以及python3.3之前,使用协程要基于greenlet或者gevent这种第三方库来实现,由于不是Python原生封装的,使用起来可能会有一些性能上的流失。但是在python3.4中,引入了标准库asyncio,直接内置了对异步IO的支持,可以很好的支持协程。可以使用asyncio库提供的@asyncio.coroutine把一个生成器函数标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。
而后为了简化并更好地标识异步IO,从Python3.5开始引入了新的语法async和await,把asyncio库的@asyncio.coroutine替换为async,把yield from替换为await,可以让coroutine的代码更简洁易读。
其中async关键字用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件消失后再回来继续执行。
await关键字用来实现任务挂起操作,比如某一异步任务执行到某一步时需要较长时间的耗时操作,就将此挂起,去执行其他的异步程序。注意:await后面只能跟异步程序或有__await__属性的对象。
假设有两个异步函数async work1和async work2,work1中的某一步有await,当程序碰到关键字await work2()后,异步程序挂起后去执行另一个异步work2函数,当挂起条件消失后,不管work2是否执行完毕,都要马上从work2函数中回到原work1函数中继续执行原来的操作。
import asyncio
import datetime
async def work1(i):
print("work1'{}'执行中......".format(i))
res = await work2(i)
print("work1'{}'执行完成......".format(i), datetime.datetime.now())
print("接收来自work2'{}'的:", res)
async def work2(i):
print("work2'{}'执行中......".format(i))
await asyncio.sleep(1.5)
print("work2'{}'执行完成......".format(i), datetime.datetime.now())
return "work2'{}'返回".format(i)
loop = asyncio.get_event_loop() # 创建事件循环
task = [asyncio.ensure_future(work1(i)) for i in range(5)] # 创建一个task列表
time1 = datetime.datetime.now()
# 将任务注册到事件循环中
loop.run_until_complete(asyncio.wait(task))
time2 = datetime.datetime.now()
print("总耗时:", time2 - time1)
loop.close()
运行结果如下:
从結果可以看出,所有任务差不多是在同一时间执行结束的,所以总耗时为1.5s,证明程序是异步执行的。
至此,Python中协程的用法已经总结完毕。在平时开发中,写协程用的最多的还是async/await或者第三方库gevent模块,yield的方式只用于写生成器。关于异步协程,Python的支持和封装也越来越完善,用起来语法也很简单,可是如果我们要透过语法去学习他们优秀的思想,却不是一件容易的事情。