异步编程(四)-----协程
什么是协程?百度上一大堆,随时可以查。我认为协程就是微线程,比线程还要小。为什么要引入协程?我们发现在线程使用中,有一个GIL锁,线程之间访问临界资源是互斥的,这都是不利于提升代码执行效率的。我们知道线程是CPU调度的最小单位,如果我们有一个线程,线程内包含多个协程,协程之间来回切换就设计不到CPU的切换,就会减小很多不必要的开销。协程和线程相比,切换是由代码的关键字完成的,代码自由度要高一些。协程的使用要比线程、进程麻烦一点。协程在处理IO繁忙型数据时,效率很高,一个线程可以并发上完个协程。说白了,在做异步爬虫时,效率很高。下边逐步开始探索。
目前python实现协程的方式大概有3中:yield关键字、asyncio模块、gevent模块。
yield这个词在操作系统中出现过,学过操作系统的同学对这个词应该不陌生。下边上代码:
def func1(): print('111111') yield print('222222') yield if __name__ == '__main__': gen1 = func1() gen1.__next__() gen1.__next__()
输出:
111111 222222
关键字yield的使用和迭代器就有关系了。在函数定义func1()有关键字yield,所以就不在是一个普通的函数定义。如果仍然用func1()调用执行函数,会发现没有任何输出。print(func1())之后,打印出来的是一个
<generator object func1 at 0x0000000003967248>
哦,generator 是生成器!如何执行这个生成器呢,就用.__next__()方法,或者用next(func1())也可以。
再来一个例子:
def func1(): print('111111') yield print('222222') yield def func2(): print('333333') yield print('444444') yield if __name__ == '__main__': gen1 = func1() gen2 = func2() gen1.__next__() gen2.__next__() gen1.__next__() gen2.__next__()
输出:
111111 333333 222222 444444
好像看起来通过yield关键字,可以实现两个函数之间的来回切换。确实,核心点在于,调用生成器方法.__next__() 会执行到函数yield部分,下次的.__next__() 会继续执行。这是比较简单的yield用法,但是能看出来确实起到协程切换的作用的了。用的不多。
gevent模块是很古老的python用于实现协程的模块,还在2.x的时代。在3.7之后,就出现asycio模块代替gevent模块了,所以gevent模块在这里就不在讨论。有新的,为什么还用旧的?早晚被淘汰。
asyncio是python3.7之后自带的,不用额外pip下载,直接在代码中import就可以。
下边着重说asyncio模块:
asyncio模块主要基于关键字async、await。
用async关键词修饰的函数叫做协程函数:
这是最基本的协程函数定义,调用。
刚才的代码可以这样写:
import asyncio async def fun(): print("hello world!") if __name__ == '__main__': reslut = fun() loop = asyncio.get_event_loop() loop.run_until_complete(reslut)
asyncio.run(reslut)
等同于:
loop = asyncio.get_event_loop()
loop.run_until_complete(reslut)
为了实现协程之间来回切换,必须创建一个“圈”,规定圈内的协程可以来回切换,这个“圈”就是asyncio.get_event_loop()创建的事件循环对象。好比是之前说的线程池,只有线程池内部的线程才可以切换。
loop.run_until_complete(reslut) 是将协程函数注册到事件循环中,也就是添加到线程池中,这样才能被执行。
再来一个例子,说明下await:
import asyncio async def fun1(): print('111111') await asyncio.sleep(1) print('222222') async def fun2(): print('333333') await asyncio.sleep(1) print('444444') if __name__ == '__main__': reslut1 = fun1() reslut2 = fun2() loop = asyncio.get_event_loop() loop.run_until_complete(reslut1) loop.run_until_complete(reslut2)
输出:
111111 222222 333333 444444
从输出上看出来,好像没有完成并发操作,仍然是串行,那是因为代码写的是串行的代码。await 后边必须要跟能够等待的对象,asyncio.sleep()就是模拟IO等待的一个函数。如果写time.sleep()会报错。
await类似于前边的yield,和中断也很像,就是等,停下来执行await后边的函数,这么说和函数调用也差不多。。。
那问题来了,你不是要实现并发吗,怎么写就可以呢?
下边要是用task对象。
import asyncio import time async def fun1(): print('111111') await asyncio.sleep(1) print('222222') async def fun2(): print('333333') await asyncio.sleep(1) print('444444') async def main(): reslut1 = fun1() reslut2 = fun2() task1 = asyncio.create_task(reslut1) task2 = asyncio.create_task(reslut2) await task1 await task2 if __name__ == '__main__': start_time = time.time() asyncio.run(main()) end_time = time.time() print(end_time-start_time)
输出:
111111 333333 222222 444444 1.0020570755004883
从输出和时间来看,两个函数fun1和fun2实现了并发执行。
result1和result2是协程对象,自己可以print一下试试。asyncio.create_task(reslut1)、asyncio.create_task(reslut2) 是创建task对象,同时将任务添加到事件循环,这里其实是做了两件事!!!!所以在代码中并没有看到有关于“将任务添加到事件循环”。
再继续:
import asyncio import time async def fun1(): print('111111') await asyncio.sleep(1) print('222222') return '1212' async def fun2(): print('333333') await asyncio.sleep(1) print('444444') return '3434' async def main(): print("main开始") reslut1 = fun1() reslut2 = fun2() task_list = [ asyncio.create_task(reslut1), asyncio.create_task(reslut2) ] done,pending = await asyncio.wait(task_list) for ret in done: print('Task ret: ', ret.result()) print("main结束") print(done) if __name__ == '__main__': start_time = time.time() asyncio.run(main()) end_time = time.time() print(end_time-start_time)
输出:
main开始 111111 333333 222222 444444 Task ret: 1212 Task ret: 3434 main结束 {<Task finished coro=<fun1() done, defined at C:/Users/W/PycharmProjects/test/class/协程.py:22> result='1212'>, <Task finished coro=<fun2() done, defined at C:/Users/W/PycharmProjects/test/class/协程.py:28> result='3434'>} 1.0020573139190674
done,pending = await asyncio.wait(task_list) 是关键代码,task_list是一个task对象的列表,asyncio.wait执行事件监听里的task对象。返回的done是一个set集合,集合中包含协程函数的返回值,也就是可以以这样的方式获取返回值,或者利用之前的回调机制,也可以。
gather也可以实现wait功能,具体方法略有不同,请参考: