Python中的Asyncio详解

/ 默认分类 / 0 条评论 / 626浏览

一.基本介绍

asyncio 是 Python 3.4 引入的一个库,用于编写并发代码。它使用单线程事件循环实现异步编程,适用于 IO 密集型任务,如网络操作、文件 IO 等。 async关键词修饰的function就不在是一个普通的python函数了,我们无法直接调用这个函数来执行逻辑。使用async修饰的python中的function是一个coroutine function,调用coroutine function获取到的是一个coroutine object。 asyncio的主要原理其实就是在单个线程中维护了一个事件循环执行器,也就是event loop。而eventloop中可以直接执行的是task,task是coroutine object之后的数据。 async修饰之后也就是得到一个调用过程,其实就是封装了一个协程。

二.详细说明

import asyncio
import time

# 定义一个异步函数 say_after
async def say_after(delay, what):
    # 异步等待指定的延迟时间
    await asyncio.sleep(delay)
    # 打印传入的字符串
    print(what)

# 定义主函数 main
async def main():
    # 打印任务开始的时间
    print(f"started at {time.strftime('%X')}")

    # 异步等待并执行 say_after 函数
    await say_after(1, 'hello')
    await say_after(2, 'world')

    # 打印任务完成的时间
    print(f"finished at {time.strftime('%X')}")

# 运行主函数 main
asyncio.run(main())

started at 22:20:15
hello
world
finished at 22:20:18

上面的async def say_after和async def main都是定义了一个coroutine function。当我们执行asyncio.run(main())的时候,就会构建出一个eventloop,run中传入的参数就是eventloop中接收到的第一个task,此时eventloop中只有一个task,也就是main,所以会直接执行main方法,打印第一句话之后,执行到await say_after,await关键词的作用是:将await后面的coroutine封装为一个task,然后告诉eventloop,当前你正在执行的task需要先停一下了,需要先来执行我这个task,并且等我这个执行完之后才能继续执行之前的task,所以进入say_after(1这个函数中,然后执行到await asyncio.sleep(1),此时同样的需要等到asyncio.sleep(1)这个task结束后才能继续执行当前的task,于是开始等待1s,1s之后,asyncio.sleep(1)这个task等于执行完毕了,于是告诉eventloop可以继续执行别的task了,于是eventloop继续执行之前的say_after(1这个task,也就是print("hello"),执行完之后say_after(1这个task也就执行完毕了,于是告诉eventloop可以执行其他的task了,此时eventloop中只剩下一个最开始的main这个task,于是继续执行main,执行到await say_after(2,同样的道理,睡2s之后,打印了world,然后继续执行main,就结束了。 整个过程花费了3s(忽略其他的简单逻辑)。

我们发现,这个似乎并没有实现协程的作用,因为当say_after(1在睡眠1s的时候,应该去执行say_after(2的,这样总共就只需要花费2s了,但是实际上却花费了3s。

import asyncio
import time

# 定义一个异步函数 say_after
async def say_after(delay, what):
    # 异步等待指定的延迟时间
    await asyncio.sleep(delay)
    # 打印传入的字符串
    print(what)

# 定义主函数 main
async def main():
    # 创建第一个任务,延迟1秒后打印 'hello'
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    # 创建第二个任务,延迟2秒后打印 'world'
    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')}")

# 运行主函数 main
asyncio.run(main())

started at 22:20:42
hello
world
finished at 22:20:44

当我们把代码修改为上面这样的时候,可以看到,会先创建好两个task,当执行到await task1的时候,此时eventloop中已经存在三个task了,main,task1,task2,所以当await task1的时候,在里面进行sleep(1)之后,eventloop发现还有一个准备好的task2,于是就执行task2,于是就执行了sleep(2),然后,当两个task都执行完毕之后,main再执行完毕。 但是事实上,await task1 和 await task2 并不会并行执行。在 await 语句中,每个任务都会被等待直到它完成。所以,这两个 await 语句是顺序执行的,而不是并行执行的。 那么为什么总的执行时间是 2 秒而不是 3 秒呢?这是因为在这种情况下,await task1 和 await task2 是同时等待的,而不是一个接一个地等待。在 await task1 和 await task2 之间没有明确的依赖关系。当 await task1 执行时,它会暂停当前协程,但不会阻止其他协程(比如 task2)的执行。因此,task1 和 task2 是并行执行的。而且,由于 task2 在 task1 之后才完成,因此总的执行时间是 2 秒,而不是 3 秒。

在 Python 中,多个协程在同一个线程中执行时,通常情况下是同一时刻只有一个任务在运行。这是因为 Python 的协程是基于事件循环的,事件循环在单线程中管理任务的执行。 在一个协程执行期间,如果遇到了阻塞的 I/O 操作(比如网络请求、文件读写等),事件循环会将这个任务挂起,并开始执行其他等待中的任务,直到这个 I/O 操作完成。这样就实现了在单线程中高效利用 I/O 等待时间,达到类似并发执行的效果。 当一个任务使用 await 等待另一个任务完成时,它会暂停当前任务的执行,将控制权交给事件循环,事件循环会继续执行其他等待中的任务,直到被等待的任务完成。 因此,尽管 Python 的协程是在单线程中执行的,但由于事件循环的调度机制,可以实现并发执行的效果。

另外,多个任务同时放入eventloop事件循环中的代码也可以这样写:

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')}")

    # 使用 asyncio.gather 并行执行两个 say_after 函数
    await asyncio.gather(
        say_after(1, 'hello'),
        say_after(2, 'world')
    )

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

asyncio.run(main())

started at 22:21:10
hello
world
finished at 22:21:12