python asyncio入门指南

引言

这篇文章是我阅读asyncio的官方文档,以及一些技术博客的学习总结。主要对python asyncio做一个总体的介绍。当你在第一次阅读asyncio的官方文档的时候你会看到大量晦涩难懂的单词和概念,阅读完这篇博客你会对许多名词有一定的理解,再去深入学习asyncio的官方文档,你会感到轻松很多,这是我写这篇文章的目的,在文章的最后我将贴出我觉得有意义的参考文章的地址。


async IO是一种并发编程技术,python的asynio指的是一个包的名称,它是一个代码库是python对async IO技术的实现。
async IO是一种单线程和单进程的设计。说到并发技术,你可能第一时间想到的是多进程和多线程,可单进程和但线程怎么实现并发呢?事实上async IO通过一个事件循环(event loop)作为一个大脑协调调度任务,它的灵魂在于并发调度任务,尽管只有一个线程,仍然能给人一种并发的感觉,当然本质上它不是真正并行。


想象有一个围棋大师与一众棋手车轮战
我们假设:

  • 棋手总共20名
  • 大师完成一步棋需要1秒
  • 棋手完成一步棋需要10秒
  • 完成一场比赛大师和棋手每人要走30步
  1. 同步版本:大师每次只和一个棋手下棋,完成比赛后再战下一位棋手,完成一场比赛需要 ( 1 + 10 ) ∗ 30 = 330 秒 (1+10)*30=330秒 (1+10)30=330,完成全部的20场比赛需要 330 ∗ 20 = 6600 秒 330*20=6600秒 33020=6600
  2. 异步版本:大师每次花费1秒钟下完一步棋之后马上切换到下一张桌子,而它的对手则在等待他回来的时间里完成自己的动作,这样大师在所有20场比赛中完成一步棋只需要 20 ∗ 1 = 20 秒 20*1=20秒 201=20,一共30步棋,完成紫塞的总时长只有 20 ∗ 30 = 600 秒 20*30=600秒 2030=600

一、初次体验

我们通过简单的示例,观察不同程序的总运行时间来感受asyncio

import time
from datetime import datetime

def work1():
    time.sleep(1)
    print('work1已完成')

def work2():
    time.sleep(1)
    print('work2已完成')

def main():
    work1()
    work2()

print("开始",datetime.now().time())
main()
print("结束",datetime.now().time())

# 开始 18:14:51.164776
# work1已完成
# work2已完成
# 结束 18:14:53.179675
import asyncio
from datetime import datetime

async def work1():
    await asyncio.sleep(1)
    print('work1已完成')

async def work2():
    await asyncio.sleep(1)
    print('work2已完成')

async def main():
    print("开始", datetime.now().time())
    await asyncio.gather(work1(),work2())
    print("结束", datetime.now().time())

asyncio.run(main())

# 开始 18:17:50.173980
# work1已完成
# work2已完成
# 结束 18:17:51.179613
  • 不用担心遇到陌生的名词,再接下来的章节中我会一一解释
  • 4行 8行 定义了两个work需要消耗1秒钟的时间来完成
  • 17行 使用asyncio.run()启动了event_loop作为协调任务的大脑,并将main()包装成第一个task传入
  • 14行 通过asyncio.gather把两个work包装为task交给大脑并行调度执行

二、什么是coroutine

coroutine的定义

coroutine包含两个概念

  • coroutine function
  • coroutine object

对于coroutine function 和 coroutine object 我们都称之为 coroutine。

1.什么是coroutine function?

首先由async def定义的函数被称为coroutine function,如下第3行代码中的async def main ()

import asyncio

async def main():    
    print('hello')
    await asyncio.sleep(1)
    print('world')

#下面的代码什么也不会做
coro_obj = main()  

asyncio.run(main())

2.什么是coroutine object?

在上面的例子中,与普通的函数不同是,第9行调用coroutine function什么也发生,coroutine function内部的语句也不会被执行,它只会默默地返回一个coroutine object。
如上所示,coro_obj = main()并不会执行,main()会返回一个coroutine object。

coroutine怎么样才能运行起来?

让coroutine运行起来方法大概分为两类

  • await它
  • 把它包装成task(什么是task下一章节会讲)

下面我将介绍几种运行coroutine的方法

1.asyncio.run()

import asyncio

async def main():    
    print('hello')
    await asyncio.sleep(1)
    print('world')

#下面的代码什么也不会做
coro_obj = main()  

asyncio.run(main())

在前面的案例中我们使用asyncio.run(main())运行了main(),而main()是一个coroutine。asyncio.run()做了两件事:

  • 首先它启动了一个event_loop(事件循环)作为顶层的大脑接管程序的运行
  • 接着他把传入coroutine包装成一个task

当一个coroutine被封装成一个task的时候,task会被交给事件循环也就是我们的大脑,大脑会尽快的寻找机会执行它。

2.await coroutine

await 也可以执行coroutine

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())
  • 16行:首先是前面提到的main()asyncio.run()包装成task在事件循环中被执行。
  • 11行:main()在其内部遇到了运行到第11行await say_after(1, 'hello'),await告诉事件循环,say_after可能是一个耗时任务
  • 5行:进入say_after()果然遇到了await asyncio.sleep(delay),它告诉事件循环(event_loop),我这里需要等待一秒种,你现在可以去处理别的任务了
started at 17:13:52
hello
world
finished at 17:13:55

3.asyncio.create_task()

前面提到asyncio.run()可以把coroutine包装成task
使用aysncio.create_task()方法也可以把coroutine包装成一个task,task会被直接交给事件循环,让我们的大脑(事件循环)尽快安排它执行。

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

    #等待两个task完成,task也可以await,后面会讲
    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")
  • 6行 7行 :asyncio.create_task()各把一个cotoutine也就是这里的say_after()包装成task
  • 这两各task直接会被交给事件循环安排执行
started at 17:14:32
hello
world
finished at 17:14:34

4.asyncio.gather()

asyncio.gather()可以一次性将多个coroutine包装成task,一起交给事件循环并发调度
它的返回结果是一个awaitable object(后面会讲),我们可以await它的返回结果来收集这些task的执行结果
文章最开始的例子,就是使用的asyncio.gather()

import asyncio
from datetime import datetime

async def work1():
    await asyncio.sleep(1)
    print('work1已完成')

async def work2():
    await asyncio.sleep(1)
    print('work2已完成')

async def main():
    print("开始", datetime.now().time())
    await asyncio.gather(work1(),work2())
    print("结束", datetime.now().time())

asyncio.run(main())

# 开始 18:17:50.173980
# work1已完成
# work2已完成
# 结束 18:17:51.179613

5.asyncio.TaskGroup()

python 3.11中的asyncio.TaskGroup能帮助我们创建并管理一组task,它比asyncio.gather()更可靠,具体参考asyncio的官方文档,这里我只做简单介绍

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

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

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

    # The await is implicit when the context manager exits.

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

三、什么是Awaitable

Awaitable( awaitable object/ 可等待对象)
如果一个对象能使用await,那它就是一个awaitable object。
主要的awaitable object有三种 coroutine、Task、Future,下面将对这三种awaitable object逐一讲解。

1.coroutine

在python中coroutine是awaitable,因此我们可以让一个coroutineawait(等待)其它coroutine完成。

import asyncio

async def nested():
    return 42

async def main():

    #这里什么也不会发生,因为 "nested()"是coroutine object
    #我们说过coroutine需要被await
    nested()

    #这里我们await了它,才会输出 "42"
    print(await nested())

asyncio.run(main())

2.Task

我们使用asyncio.create_task可以把coroutine包装成task,同时task也是一个awaitable。

import asyncio

async def nested():
    return 42

async def main():

    #被包装成了task的nested()很快就会被按安排运行
    task = asyncio.create_task(nested())

    #我们这里拿着task的引用可以取消task也可以等待它完成
    await task

asyncio.run(main())

3.Future

Future是一种低级(low level)的Awaitable,它表示异步操作的最终结果
在一个coroutine中await一个Future意味着,我们的coroutine要等待Future完成
task实际上是Future的一个子类哦,但在应用层面我们不需要自己创建,也不需要过多的关注Future。

三、Task的创建和使用

经过前面的学习我们知道task可以被事件循环直接安排执行,同时事件循环会帮助我们并发调度多个task,因此创建task是我们在进行asyncio并发编程过程中非常常用的操作,下面我们来回顾一下我们创建task的三种方法

  • asyncio.create_task()
  • asyncio.gather()
  • asyncio.TaskGroup.create_task()

在这里我只是带你了解这些方法的概念和基础用法,更多的可选参数以及细节,还是希望你能够去学习和参考python asyncio的官方文档。
接下来我将介绍的是几种task的使用技巧

1.如何保护长期task?

如果你想创建一个长期运行的后台task,维持好对task的引用非常重要,事件循环(event loop)只持有对task的弱引用,为了避免我们的长期任务被回收,需要将它们保存在集合中。

background_tasks = set()

for i in range(10):
    task = asyncio.create_task(some_coro(param=i))

    # Add task to the set. This creates a strong reference.
    background_tasks.add(task)

    # To prevent keeping references to finished tasks forever,
    # make each task remove its own reference from the set after
    # completion:
    task.add_done_callback(background_tasks.discard)

2.task的取消与清理工作

使用task的引用我们可以轻松的取消task,当一个task被取消的时候将会抛出asyncio.CancelledError,我们在coroutine function内部增加try/finally来捕获它并且通过finally来执行一些清理工作是很有必要的

import asyncio

async def my_coroutine():
    try:
        # 假设这是一些长时间运行的操作
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        # 可以在这里处理取消事件,但通常你会让异常继续传播
        raise
    finally:
        # 这里是清理代码,无论是否取消都会执行
        print("Clean-up!")

async def main():
    # 创建任务
    task = asyncio.create_task(my_coroutine())

    # 给任务一点时间去启动
    await asyncio.sleep(1)

    # 取消任务
    task.cancel()

    try:
        # 等待任务完成,这里会抛出 asyncio.CancelledError
        await task
    except asyncio.CancelledError:
        print("Task was cancelled")

asyncio.run(main())

四、asyncio设计模式

下面介绍两种asyncio程序的设计模式

Chaining Corotunies

我们可以将corotunie链接起来组合成一个任务链,链上的任务自上而下形成层级关系,同时运行多条任务链又不会导致堵塞,下事件循环的指挥下见缝插针并且井然有序的完成各自的工作。

#!/usr/bin/env python3
# chained.py

import asyncio
import random
import time

async def part1(n: int) -> str:
    i = random.randint(0, 10)
    print(f"part1({n}) sleeping for {i} seconds.")
    await asyncio.sleep(i)
    result = f"result{n}-1"
    print(f"Returning part1({n}) == {result}.")
    return result

async def part2(n: int, arg: str) -> str:
    i = random.randint(0, 10)
    print(f"part2{n, arg} sleeping for {i} seconds.")
    await asyncio.sleep(i)
    result = f"result{n}-2 derived from {arg}"
    print(f"Returning part2{n, arg} == {result}.")    return result

async def chain(n: int) -> None:
    start = time.perf_counter()
    p1 = await part1(n)
    p2 = await part2(n, p1)
    end = time.perf_counter() - start
    print(f"-->Chained result{n} => {p2} (took {end:0.2f} seconds).")

async def main(*args):
    await asyncio.gather(*(chain(n) for n in args))

if __name__ == "__main__":
    import sys
    random.seed(444)
    args = [1, 2, 3] if len(sys.argv) == 1 else map(int, sys.argv[1:])
    start = time.perf_counter()
    asyncio.run(main(*args))
    end = time.perf_counter() - start
    print(f"Program finished in {end:0.2f} seconds.")
  • 8行:part1是一个耗时任务,需要消耗随机生成的一定事件后返回结果。
  • 16行:part2也是一个消耗随机时间并返回结果的耗时任务,不过part2要依赖part1的返回结果,part1运行完毕,part2才可以开始。
  • 24行:定义了一个任务链,part1返回结果给part2,定义了依赖关系。
  • 31行:我们通过main函数来生成多个任务链,并把他们交给主时间循环。

Producer-Consumer

借助asyncio.Queue()队列,我们可以构建一个生产者消费者模型。

import asyncio, random
 
async def rnd_sleep(t):
    # sleep for T seconds on average
    await asyncio.sleep(t * random.random() * 2)
 
async def producer(queue):
    while True:
        # produce a token and send it to a consumer
        token = random.random()
        print(f'produced {token}')
        if token < .05:
            break
        await queue.put(token)
        await rnd_sleep(.1)
 
async def consumer(queue):
    while True:
        token = await queue.get()
        # process the token received from a producer
        await rnd_sleep(.3)
        queue.task_done()
        print(f'consumed {token}')
 
async def main():
    queue = asyncio.Queue()
 
    # fire up the both producers and consumers
    producers = [asyncio.create_task(producer(queue))
                 for _ in range(3)]
    consumers = [asyncio.create_task(consumer(queue))
                 for _ in range(10)]
 
    # with both producers and consumers running, wait for
    # the producers to finish
    await asyncio.gather(*producers)
    print('---- done producing')
 
    # wait for the remaining tasks to be processed
    await queue.join()
 
    # cancel the consumers, which are now idle
    for c in consumers:
        c.cancel()
 
asyncio.run(main())

五、结语

asyncio还有更多的用法和细节,比如asyncio.timeout等等都没有介绍到,但是这篇文章的目的不是做一篇保姆级的详细教程。正如我开篇所说,我希望更借助这篇文章能够帮助大家了解asyncio的一些概念和基础用法,为大家深入学习asyncio的官方文档打下基础。

asyncio官方文档
https://docs.python.org/3/library/asyncio.html
realpython关于asyncio的博客
https://realpython.com/async-io-python/
asyncio生产者消费者模型的实现博客
https://stackoverflow.com/questions/52582685/using-asyncio-queue-for-producer-consumer-flow

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值