重新谈异步编程-协程

首先来说什么是协程?

协程又被称之为是微线程,或者说是在一个线程内实现代码块的相互切换执行。

在《计算机操作系统》中我们学过,一个进程中包含若干个线程,一个线程中可以包含若干个进程。在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……

原文地址:https://www.cnblogs.com/lgwdx/p/15508963.html