首先来说什么是协程?
协程又被称之为是微线程,或者说是在一个线程内实现代码块的相互切换执行。
在《计算机操作系统》中我们学过,一个进程中包含若干个线程,一个线程中可以包含若干个进程。在Python中,一个线程又包含若干个协程。CPU如果在进程和进程之间切换,开销是比较大的,相对来讲,同一进程下的线程切换开销要小很多,因为同一进程下的线程是共享该进程的资源的。同理,同一线程下的协程切换,也要比线程切换开销小。
协程不像线程一样,需要创建线程实例化对象,协程是通过协程函数+将协程调入事件循环实现的。
协程函数的定义关键词是asyncio
例如:
import asyncio async def fun(): pass
这就是一个协程函数的定义,asyncio模块是内置的,不用额外下载安装。但是协程函数不能像普通函数一样,“fun()”,可以运行,而是需要将协程函数注册到事件循环当中,并且协程函数中必须有“await+可等待对象(协程对象、Future对象、Task对象)”。
例如:
import asyncio async def fun(): print('hello') await asyncio.sleep(2) print('word') loop = asyncio.get_event_loop() loop.run_until_complete(fun())
其中,asyncio.get_event_loop()是创建事件循环对象,loop.run_until_complete(fun())是将协程函数fun()注册到事件循环当中,并运行。结果是没有问题的,输出'hello',等待2s,在输出'word‘。
当中的asyncio.sleep(2),是模拟IO工作2s,await是将这个操作挂起2s中,或者说这个操作被阻塞2s的时间。这里的asyncio.sleep(2)不能写成time.sleep(2),两个虽然都是等待2s,但是实质是不一样的。
再py3.7之后,我们可以将
loop = asyncio.get_event_loop()
loop.run_until_complete(fun())
合并为:
asyncio.run(fun())
============================================================================================================================
现在,我们在创建一个新的协程函数。
import asyncio import time async def fun(): print('hello') await asyncio.sleep(2) print('word') async def fun1(): print('how are you?') await asyncio.sleep(2) print('fine') start_time = time.time() loop = asyncio.get_event_loop() loop.run_until_complete(fun()) loop.run_until_complete(fun1()) end_time = time.time() print('耗时',end_time-start_time)
输出:
hello
word
how are you?
fine
耗时 4.002251625061035
我们发现,虽然是将两个协程函数分别注册到了时间循环当中,但是并没有像我们想象中一样,交替执行,而是仍然串行执行,消耗4s。我们希望是fun协程先执行print('hello'),遇到了一个2s的IO操作,马上切换到协程fun1的print('how are you?'),又碰到IO操作,继续将IO操作挂起。经过不到2s的时间后协程fun的IO已经结束了,继续执行fun的print('word'),再执行fun1的print('fine')。为什么是这样?我个人猜测和理解,并没有证明,loop.run_until_complete(fun())、loop.run_until_complete(fun1())是先后注册到协程中的,fun()结束之后,才运行fun1()。如果有其他解释欢迎留言。这里我们还要正确理解await后边的IO操作。IO操作是可以和CPU并行进行的,也就是说在同一时刻,既可以CPU计算,又可以IO工作。如果说在IO的时候,CPU在等待,很明显浪费时间,所以我们希望有IO和CPU并行进行,也就是有IO的协程之间并发。
============================================================================================================================
那我们如何将两个协程并发起来呢?依照上边的思想,我们希望将两个协程同时注册到事件循环当中。
例:
import asyncio import time async def fun(): print('hello') await asyncio.sleep(2) print('word') async def fun1(): print('how are you?') await asyncio.sleep(2) print('fine') start_time = time.time() task1 = asyncio.ensure_future(fun()) task2 = asyncio.ensure_future(fun1()) task_list = [task1,task2] loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(task_list)) end_time = time.time() print('耗时',end_time-start_time)
输出:
hello
how are you?
word
fine
耗时 2.0022101402282715
我们发现,这是我们需要的。代码中的asyncio.ensure_future()用来创建task对象,task_list是一个列表,列表里边每个元素是task对象。
我们打印一下asyncio.wait(task_list),输出:<coroutine object wait at 0x000001E8C9B523C8>,发现这是一个协程对象(注意不是协程函数)。
这里有同学说了,我们将协程函数转成协程对象不就可以了?print(fun()),发现fun()确实是协程对象,但是会报警告,“Enable tracemalloc to get the object allocation traceback”。
所以在这里,实际上是将协程对象先转成了task对象,再将task对象组成的列表传入到loop.run_until_complete()里。
所以到此为止我们知道了,loop.run_until_complete()这个方法的参数必须是一个协程对象。
我们可以将asyncio.wait(task_list)改为,res = asyncio.gather(*task_list)也可以,wait方法要求传入的是一个列表,gather方法要求传入的参数是一个列表解包后的结果。所以也可以这样写:asyncio.gather(task1,task2)。
import asyncio import time async def fun(): print('hello') await asyncio.sleep(2) print('word') async def fun1(): print('how are you?') await asyncio.sleep(2) print('fine') start_time = time.time() task1 = asyncio.ensure_future(fun()) task2 = asyncio.ensure_future(fun1()) task_list = [task1,task2] loop = asyncio.get_event_loop() res = asyncio.gather(task1,task2) print(res) loop.run_until_complete(res) end_time = time.time() print('耗时',end_time-start_time)
输出:
<_GatheringFuture pending>
hello
how are you?
word
fine
耗时 2.0014095306396484
这里我们发现小细节,asyncio.gather(task1,task2)的返回值并不是前边说的task类对象,而是一个Future类对象,这是因为task是Future的子类,这下就都明白了吧?pending的意思是未开始,打印的时候还没有开始run呢。所以说gather比wait更高一级。
那么task对象到底有什么用?task对象是把协程对象进行封装,可以追踪 coroutine协程对象的完成状态。也就是说保存了协程运行后的状态,用于未来获取协程的结果。
到此,我们知道了loop.run_until_complete()的参数可以是Future对象,也可以是协程对象。回想第一个例子,我们把fun()传入到这里不就可以解释了吗?
============================================================================================================================
现在,我们来说说ensure_future和create_task区别。
两个方法都是来创建task对象,但是如果把上边的ensure_future修改为create_task会报错。为啥?
注意:如果当前线程中没有正在运行的事件循环,asyncio.create_task将会引发RuntimeError异常。可以理解为asyncio.create_task必须写在一个协程函数中。
将上面的代码略微修改就可以:
import asyncio import time async def fun(): print('hello') await asyncio.sleep(2) print('word') async def fun1(): print('how are you?') await asyncio.sleep(2) print('fine') async def main(): start_time = time.time() task1 = asyncio.create_task(fun()) task2 = asyncio.ensure_future(fun1()) await asyncio.gather(task1,task2) end_time = time.time() print('耗时',end_time-start_time) loop = asyncio.get_event_loop() loop.run_until_complete(main())
输出:
hello
how are you?
word
fine
耗时 2.000105619430542
在main()协程函数中,await是必须要有的,如果没有await,这就不是一个正确的协程函数,会报错。后边的asyncio.gather(task1,task2)前边介绍过,返回的是一个Future对象,是没问题的。我是从实用角度解释的ensure_future和create_task区别,并不全面,后续碰到实际问题再实际分析。
============================================================================================================================
刚才说的到的asyncio.wait 和 asyncio.gather,有什么区别呢?
前者的返回值是两个集合结果,第1个集合是已经完成的task,第2个集合是未完成的task。后者直接返回的是多个task的计算结果。
例:
import asyncio import time async def fun(): print('hello') await asyncio.sleep(2) print('word') async def fun1(): print('how are you?') await asyncio.sleep(2) print('fine') async def main(): start_time = time.time() task1 = asyncio.create_task(fun()) task2 = asyncio.ensure_future(fun1()) res1,res2 = await asyncio.wait([task1,task2]) print(res1) print('======') print(res2) end_time = time.time() print('耗时',end_time-start_time) loop = asyncio.get_event_loop() loop.run_until_complete(main())
输出:
hello how are you? word fine {<Task finished coro=<fun() done, defined at C:/Users/王栋轩/PycharmProjects/pythonProject1/main.py:98> result=None>, <Task finished coro=<fun1() done, defined at C:/Users/王栋轩/PycharmProjects/pythonProject1/main.py:102> result=None>} ====== set() 耗时 2.000847101211548 进程已结束,退出代码0
很容看到,返回值的第1个是2个协程对象,第2个是空。我们可以使用task.result来获取两个task对象(两个task对象是由前边的两个协程函数封装好的)返回值。
例:
import asyncio import time async def fun(): print('hello') await asyncio.sleep(2) print('word') return 'fun1' async def fun1(): print('how are you?') await asyncio.sleep(2) print('fine') return 'fun2' async def main(): start_time = time.time() task1 = asyncio.create_task(fun()) task2 = asyncio.ensure_future(fun1()) res1,res2 = await asyncio.wait([task1,task2]) for i in res1: print(i.result()) end_time = time.time() print('耗时',end_time-start_time) loop = asyncio.get_event_loop() loop.run_until_complete(main())
输出:
hello how are you? word fine <class '_asyncio.Task'> fun2 <class '_asyncio.Task'> fun1 耗时 2.0008704662323 进程已结束,退出代码0
例:
import asyncio import time async def fun(): print('hello') await asyncio.sleep(2) print('word') return 'fun1' async def fun1(): print('how are you?') await asyncio.sleep(2) print('fine') return 'fun2' async def main(): start_time = time.time() task1 = asyncio.create_task(fun()) task2 = asyncio.ensure_future(fun1()) res1,res2 = await asyncio.gather(*[task1,task2]) print(res1,res2) end_time = time.time() print('耗时',end_time-start_time) loop = asyncio.get_event_loop() loop.run_until_complete(main())
输出:
hello
how are you?
word
fine
fun1 fun2
耗时 2.000060558319092
可以看到,代码中用res1和res2分别接收task1和task2的返回结果。
ok。到此,协程的又一步学习告一段落。
耗时1个 晚上+一个1下午。
ending……