python协程asyncio的个人理解

协程与任务

python语境中,协程 coroutine 的概念有两个:协程函数、协程对象,协程对象由协程函数创建得到(类似于类实例化得到一个对象).

理解协程,最重要的是了解事件循环和任务执行的机制,下面是三个原则:

  • 事件循环中,不断循环执行各个任务,若一个任务遇到await或者执行完成,则返回控制权给事件循环,这时候事件循环再去执行下一个任务
  • 事件循环同一时刻只会运行一个任务
  • 协程不会被加入事件循环的执行日程,只有被注册为任务之后,事件循环才可以通过任务来设置日程以便并发执行协程

基本语法

协程的声明和运行

使用async def语句定义一个协程函数,但这个函数不可直接运行

async def aaa():
    print('hello')

print(aaa())

# 输出----------------------------------
<coroutine object aaa at 0x7f4f9a9dfec0>
/root/Project/test01/test2.py:4: RuntimeWarning: coroutine 'aaa' was never awaited
  print(aaa())
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

如何运行一个协程呢,有三种方式:

  1. 使用asyncio.run()函数,可直接运行
import asyncio

async def aaa():
    print('hello')

asyncio.run(aaa())
# 输出-------------------------
hello
  1. 使用await进行异步等待

在协程函数中最重要的功能是使用await语法等待另一个协程,这将挂起当前协程,直到另一个协程返回结果。

await的作用:挂起 coroutine 的执行以等待一个 awaitable 对象。 只能在 coroutine function 内部使用。

import asyncio

async def aaa():
    print('hello')

async def main():
    await aaa()

asyncio.run(main())
  1. 使用asyncio.create_task() 函数来创建一个任务,放入事件循环中
import asyncio

async def aaa():
    print('hello')

async def main():
    asyncio.create_task(aaa())

asyncio.run(main())

可等待对象

上面说过,协程函数中最重要的功能是使用await语法等待另一个协程,这将挂起当前协程,直到另一个协程返回结果。(重要,重复一遍)

await后面需要跟一个可等待对象(awaitable),有下面三种可等待对象:

  • 协程:包括协程函数和协程对象
  • 任务:通过asyncio.create_task()函数将协程打包为一个任务
  • Futures:特殊的 低层级 可等待对象,表示一个异步操作的 最终结果

运行asyncio程序

asyncio.run(coro, ***, debug=False)

传入协程coroutine coro ,创建事件循环,运行协程返回结果,并在结束时关闭,应当被用作 asyncio 程序的主入口点。

创建任务

asyncio.create_task(coro, ***, name=None)

将 coro 协程 打包为一个 Task 排入日程准备执行。返回 Task 对象。

休眠

coroutine asyncio.sleep(delay, result=None, ***, loop=None)

阻塞 delay 指定的秒数,该协程总是会挂起当前任务,以允许其他任务运行

机制解析

通过官网的两段代码,来详细解析一下协程的运行机制。

官方两个代码如下,注意看输出差异:

代码1,通过协程对象来执行

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")

    await say_after(1, 'hello')
    await say_after(2, 'world')

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())				# 1: 创建事件循环,传入入口点main()协程对象,此时生成一个对应的task

输出为:

started at 17:13:52
hello
world
finished at 17:13:55

代码2,通过任务来执行

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    task2 = asyncio.create_task(
        say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")

    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

输出:

started at 17:14:32
hello
world
finished at 17:14:34

注意到运行时间比前一个代码快1秒,下面说明为什么出现这种情况(文字比较多)。

代码一的运行逻辑:

asyncio.run(main()) 启动一个事件循环,将入口点main()协程对象传入,生成一个对应的任务task_main;

事件循环运行任务task_main,然后执行第1条代码:print(f”started at {time.strftime(‘%X’)}”);

接着执行第2条代码:await say_after(1, ‘hello’),第2条代码首先生成一个say_after(1, ‘hello’)协程对象,同时生成该协程对象对应的task_1;

由于await语法,task_main任务将控制权返回给事件循环,同时告诉事件循环需要等待task1才能继续运行;

事件循环获得控制权后,发现此时有两个任务task_main和task1,同时task_main在等待task1,于是会去执行task1任务;

task1任务将执行第1条代码:await asyncio.sleep(1),同样会生成asyncio.sleep(1)协程对象,以及对应的任务task2,同时因await语法将控制权返回给事件循环;

事件循环获得控制权后,发现此时有三个任务task_main、task1、task2,由于task_main、task1都处于等待状态,于是执行task3;

task3在1秒后运行完成,返回控制权给事件循环;

事件循环获得控制权,发现此时有两个任务task_main和task1,同时task_main在等待task1,于是会去执行task1任务;

task1任务执行第2条代码:print(‘hello’),执行完成后,任务也运行结束,将控制权返回给事件循环;

事件循环获得控制权后,发现此时有一个任务task_main,于是接着执行下一条代码:await say_after(2, ‘world’),继续重复上述过程,直到这个协程任务结束;

task_main执行最后一条代码;

事件循环关闭退出;

代码二的运行逻辑:

asyncio.run(main()) 启动一个事件循环,将入口点main()协程对象传入,生成一个对应的任务task_main;

事件循环运行任务task_main,然后执行前几条代码,创建两个任务task1、task2,并注册到事件循环中(此时事件循环一共有3个task),随之执行程序直到await;

第一个await:await task1,这里会阻塞当前任务task_main并将控制权返回给事件循环,事件循环获取控制权,安排执行下一个任务task1;

task1任务开始执行,直至遇到await asyncio.sleep(1),asyncio.sleep(1)协程对象开始异步执行,同时task1返回控制权给事件循环,事件循环获取控制权后安排执行下一个任务task2;

task2任务开始执行,直至遇到await asyncio.sleep(2),asyncio.sleep(2)协程对象开始异步执行,同时task2返回控制权给事件循环,事件循环获取控制权后安排执行下一个任务;

此时3个任务均处于await状态,事件循环保持等待;

1秒后asyncio.sleep(1)执行完成,task1取消阻塞,事件循环将安排task1执行,task1执行完成后返回控制权给事件循环,此时事件循环中一共两个任务task_main、task2。

此时task2任务处于await状态,而task_main也取消了阻塞,事件循环安排task_main执行,执行一行代码后遇到await task2,于是返回控制权给事件循环;

此时2个任务均处于await状态,事件循环保持等待;

1秒后asyncio.sleep(2)执行完成,task2取消阻塞,事件循环将安排task2执行,task2执行完成后返回控制权给事件循环,此时事件循环中只剩任务task_main;

于是事件循环安排task_main执行,task_main执行完成,asyncio.()函数收到信息也结束运行,整个程序结束

运行的流程图示

(任务就绪后,就等待事件循环来调用了,此时需要await来阻塞主任务task_main,否则控制权一直在task_main手上,导致task_main任务执行完成,run()收到main()执行结束的消息后,事件循环也关闭并结束,程序也将退出)

其实将第2个代码中的await task1删除,只保留await task2,结果中的输出相同,并消耗相同的总时间。但只保留await task1的话,将没有task2的输出;
如果将第2个代码中的await task1和await task2都删除,换成await asyncio.sleep(3),一样会打印相同输出,不过总时间会变为3秒;

其中的原因需要理解协程的工作机制(事件循环和控制权)

页面下部广告
分类: