Python异步编程之协程

目录

1、异步编程概述

2、Python协程

2.1、异步框架模型

2.2、协程的创建和顺序执行

2.3、多任务协程的创建和异步执行

2.3.1、创建任务

2.3.2、使用await关键字多任务异步执行

2.3.3、使用asyncio.wait()函数多任务异步执行

2.3.4、使用asyncio.gather()函数多任务异步执行

2.4、在多线程和多进程中执行

1、异步编程概述

官方文档事件循环 — Python 3.12.0 文档

  • 1、异步编程是一种并发编程的模式,在不同任务之间进行调度切换执行,减少CPU的空闲时间一达到减小整个程序的执行时间。
  • 2、与多线程和多进程并发编程模式相比,异步编辑就是协程并发执行的处理,即在同一个线程下不同任务之间的调度,所以异步编程无法充分利用多核CPU的优势,适用于IO阻塞性任务。
  • 3、异步编程的可应用于协程控制执行网络IO和IPC控制子进程、通过队列实现分布式任务、同步并发代码

2、Python协程

2.1、异步框架模型

1、python提供了asyncio模块来支持异步编程,其中涉及到coroutinesevent loopstaskfutures四个核心概念;

  • 协程(Coroutines):协程是一个特殊的函数,使用async def关键字定义。在协程中可以使用await关键字(如果一个对象可以在 await 语句中使用,那么它就是 可等待 对象。许多 asyncio API 都被设计为接受可等待对象。 可等待 对象有三种主要类型:协程, 任务 和 Future.)来暂停执行,等待另一个协程完成或者等待I/O操作完成。协程可以看作是一种轻量级的线程,它不会阻塞整个程序的执行。
  • 事件循环(Event Loop):事件循环是asyncio的核心,它负责管理和调度所有的协程。事件循环会循环运行,处理挂起的协程和I/O事件。通过事件循环,可以实现协程的调度、I/O操作的非阻塞执行以及任务的并发处理。
  • 任务(Tasks):任务是对协程的进一步封装,它可以包含一个协程对象,并在事件循环中被调度执行。通过创建任务,可以更方便地管理和控制协程的执行。
  • Future:在asyncio中,Future对象表示一个尚未完成的计算。它们通常用于异步操作,例如在网络请求或I/O操作之后。这些操作完成后,Future对象会得到一个结果。

2、异步编程框架的整个执行过程:

  • 首先事件循环启动之后,会从任务队列获取第一个要执行的coroutine,并随之创建对应task和future;
  • 然后在task的执行过程中,当遇到coroutine内部需要切换任务的地方(即使用了await关键字),task暂停执行,并释放执行线程给event loop进行调度,event loop获取下一个待执行的coroutine完成相应的初始化后执行该coroutine;
  • 当event loop执行完队列中的最后一个coroutine才会切换到第一个coroutine继续执行;
  • 当有task的执行结束,event loops会将该task移出待执行队列,对应的执行结果会同步到future中,最后持续到所有的task执行结束;

2.2、协程的创建和顺序执行

import asyncio

# 定义协程函数
async def my_coroutine(delay, name):
    # 协程的具体实现
    print(f"{name} start my_coroutine")
    await asyncio.sleep(delay)
    print(f"{name} end my_coroutine")

# 定义协程函数
async def main():
    # 等待my_coroutine(1, 'hello')执行完成
    await my_coroutine(1, 'hello')
    # 等待my_coroutine(2, 'world')执行完成
    await my_coroutine(2, 'world')

# 创建事件循环对象
loop = asyncio.get_event_loop()
# 添加任务到事件循环中
loop.run_until_complete(main())

# 等价于上面两行代码,推荐使用
# asyncio.run(main())
  • 输出结果:

  • 注意事项:
    • 1、上面异步函数main中实际上并没有异步执行my_coroutine函数,是等待my_coroutine(1, 'hello')执行完成后再执行my_coroutine(2, 'world'),如果需要异步执行my_coroutine函数,就需要使用asyncio.create_task()将my_coroutine函数添加到task中。
    • 2、使用asyncio.run(),就可完成事件循环对象的创建和添加任务到事件循环中。

2.3、多任务协程的创建和异步执行

说明:创建task任务将my_coroutine函数添加到task中,把上面的代码改成多任务异步执行过程,所谓的多任务异步执行就是:

  • 当执行my_coroutine(1, 'hello')时先输出:“hello start my_coroutine”,遇到await asyncio.sleep(1)就挂起等待该协程执行;
  • 然后将当前线程切换到my_coroutine(2, 'world')任务执行先输出:“world start my_coroutine”,同样遇到await asyncio.sleep(2)也挂起等待该协程执行;
  • 此时再切换到my_coroutine(1, 'hello')执行,await asyncio.sleep(1)已执行完成输出“hello end my_coroutine”将该任务移出待执行队列;
  • 最后再切换到 my_coroutine(2, 'world')直至执行完成输出:“world end my_coroutine”。

2.3.1、创建任务

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

  • 将 coro 协程 封装为一个 Task 并调度其执行。返回 Task 对象。
  • 注意:
    • 在Python3.11中新增了任务组特性,即使用syncio.TaskGroup 持有一个任务分组的 异步上下文管理器。 可以使用 create_task() 将任务添加到分组中。 当该上下文管理器退出时所有任务都将被等待。
    • asyncio.TaskGroup.create_task() 是一个平衡了结构化并发的新选择;它允许等待一组相关任务并具有极强的安全保证。
  • 使用asyncio.create_task将上面的main()函数修改为如下:
# 定义协程函数
async def main():
    # 创建task1
    task1 = asyncio.create_task(
        my_coroutine(1, 'hello'))
    # 创建task2
    task2 = asyncio.create_task(
        my_coroutine(2, 'world'))

    # TODO: 将task1和task2添加到事件循环中执行

2.3.2、使用await关键字多任务异步执行

  • 说明:使用await关键字将task1和task2放入事件循环中执行,完整代码如下:
import asyncio
async def my_coroutine(delay, name):
    # 协程的具体实现
    print(f"{name} start my_coroutine")
    await asyncio.sleep(delay)
    print(f"{name} end my_coroutine")

# 定义协程函数
async def main():
    task1 = asyncio.create_task(
        my_coroutine(1, 'hello'))

    task2 = asyncio.create_task(
        my_coroutine(2, 'world'))
    # 异步执行task1、task2
    await task1
    await task2

asyncio.run(main())

# 执行结果
"""
hello start my_coroutine
world start my_coroutine
hello end my_coroutine
world end my_coroutine
"""

2.3.3、使用asyncio.wait()函数多任务异步执行

coroutine asyncio.wait(aws, *, timeout=None, return_when=ALL_COMPLETED)

  • 并发地运行 aws 可迭代对象中的 Future 和 Task 实例并进入阻塞状态直到满足 return_when 所指定的条件。
  • aws 可迭代对象必须不为空。
  • 与 wait_for() 不同,wait() 在超时发生时不会取消可等待对象。
  • 返回两个 Task/Future 集合: (done, pending)。使用如下:
    • done:是已经执行完的任务的信息的集合,可以获取任务的返回值(主要作用),
    • pending:是包含未完成任务的信息的集合

done, pending = await asyncio.wait(aws)

  • return_when 指定此函数应在何时返回。它必须为以下常数之一:

  •  完整代码实现如下:
async def my_coroutine(delay, name):
    # 协程的具体实现
    print(f"{name} start my_coroutine")
    await asyncio.sleep(delay)
    print(f"{name} end my_coroutine")

# 定义协程函数
async def main():
    task1 = asyncio.create_task(
        my_coroutine(1, 'hello'))

    task2 = asyncio.create_task(
        my_coroutine(2, 'world'))
    
    task = [task1, task2]
    await asyncio.wait(task)

asyncio.run(main())

# 执行结果
"""
hello start my_coroutine
world start my_coroutine
hello end my_coroutine
world end my_coroutine
"""

2.3.4、使用asyncio.gather()函数多任务异步执行

awaitable asyncio.gather(*aws, return_exceptions=False)

  • 并发 运行 aws 序列中的 可等待对象。
  • 若 aws 中的某个可等待对象为协程,它将自动被作为一个任务调度。
  • 若所有可等待对象都成功完成,结果将是一个由所有返回值聚合而成的列表。结果值的顺序与 aws 中可等待对象的顺序一致。
  • 若 return_exceptions 为 False (默认),所引发的首个异常会立即传播给等待 gather() 的任务。aws 序列中的其他可等待对象 不会被取消 并将继续运行。
  • 若 return_exceptions 为 True,异常会和成功的结果一样处理,并聚合至结果列表。
  • 若 gather() 被取消,所有被提交 (尚未完成) 的可等待对象也会 被取消。
  • 若 aws 序列中的任一 Task 或 Future 对象 被取消,它将被当作引发了 CancelledError 一样处理 -- 在此情况下 gather() 调用 不会 被取消。这是为了防止一个已提交的 Task/Future 被取消导致其他 Tasks/Future 也被取消。

完整代码实现如下:

async def my_coroutine(delay, name):
    # 协程的具体实现
    print(f"{name} start my_coroutine")
    await asyncio.sleep(delay)
    print(f"{name} end my_coroutine")

# 定义协程函数
async def main():
    results = await asyncio.gather(my_coroutine(1, 'hello'), my_coroutine(2, 'world'))
    print(results)

asyncio.run(main())


# 执行结果
"""
hello start my_coroutine
world start my_coroutine
hello end my_coroutine
world end my_coroutine
"""

2.4、在多线程和多进程中执行

awaitable loop.run_in_executor(executor, func, *args)

  • 在指定的执行器executor中调用 func 。
  • 异步编程要求具体的任务必须是coroutine,也就是要求方法是异步的,否则只有任务执行完了,才能将控制权释放给event loop;
  • python中的concurent.futures提供了ThreadPoolExecutor和ProcessPoolExecutor,可以让普通任务直接在异步编程中使用,从而可以把普通任务放在单独的线程或者进程中执行任务;
  • 注意:这里是使用单独开辟的线程或进程执行非异步任务。

示例代码如下:

import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def task(name, delay):
    n = 3
    while n:
        print(f"{name}: {n}")
        time.sleep(delay)
        n -= 1

async def main():
    loop = asyncio.get_running_loop()

    # 使用loop默认的执行器执行(使用的是线程池)
    fs = [loop.run_in_executor(None, task, *args) for args in [("A", 3), ("B", 2), ("C", 1)]]
    await asyncio.wait(fs)

    # # 使用线程池执行非异步任务task
    # with ThreadPoolExecutor(max_workers=3) as pool:
    #     fs = [loop.run_in_executor(pool, task, *args) for args in [("A", 3), ("B", 2), ("C", 1)]]
    #     await asyncio.wait(fs)
    #
    # # 使用进程池执行非异步任务task
    # with ProcessPoolExecutor(max_workers=1) as pool:
    #     fs = [loop.run_in_executor(pool, task, *args) for args in [("A", 3), ("B", 2), ("C", 1)]]
    #     await asyncio.wait(fs)


if __name__ == '__main__':
    asyncio.run(main())
    
# 执行结果
"""
A: 3
B: 3
C: 3
C: 2
B: 2
C: 1
A: 2
B: 1
A: 1
""""
  • 在上面代码中使用线程池执行task,在线程池中创建了3个线程,所以实际执行时每个任务会对应一个线程执行,此时是并行执行的;如果将参数max_workers=1此时只有一个线程,实际执行时是串行执行即等A执行完再执行B,B执行完再执行C;使用进程池同理。
  • 注意:在使用ProcessPoolExecutor模块时,asyncio.run(main())执行时需要设置入口点保护(if __name__ == '__main__');这是因为:
    • 当Python文件被作为主模块执行时,操作系统会将其中的所有顶级代码复制到新的子进程中。这意味着,如果直接运行Python文件,那么其中的所有顶级代码都会被复制并执行。这可能不是你想要的结果,特别是当你的代码中包含一些不应该在子进程中运行的代码时。
    • 通过将代码放在一个函数或类的定义中,只有定义在函数或类中的代码会被复制到新的子进程中。其他不在函数或类定义中的代码不会被复制。这样你就可以更好地控制哪些代码会被执行,哪些代码不会被执行。
    • 使用if __name__ == '__main__'保护入口点是一种常见的Python编程模式,可以确保你的代码在直接运行时才会被执行,而在被import时不会被执行。这样可以避免一些不必要的错误和混乱。
  • 18
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python 异步编程是一种编程范式,它利用并发来提高程序的执行效率,特别是在处理I/O密集型任务时,比如网络请求、文件操作等。异步编程的核心在于避免了线程或进程切换带来的开销,让程序能够更高效地处理多个任务。 在Python中,异步编程主要通过以下几个库来实现: 1. **asyncio**:这是Python标准库的一部分,提供了创建异步任务和协程的基础。通过`async`和`await`关键字,可以编写协程(coroutine),这些是可以在事件循环中运行的轻量级代码块。 2. **Future 和 Task**:`asyncio.Future`和`asyncio.Task`用于封装异步操作的结果,Task是Future的包装器,提供了一些额外的功能,如跟踪状态和取消操作。 3. **Coroutines**(协程):通过定义带有`async def`的函数,函数内部可以使用`await`来挂起执行,直到依赖的异步操作完成。 4. **AIO库**(如Aiohttp、aioredis等):这些第三方库针对特定场景提供了异步版本,如Aiohttp用于非阻塞的HTTP客户端,aioredis用于异步操作Redis数据库。 5. **异步装饰器**:如`@aio.coroutine`(在Python 3.5及更早版本中使用)或`async def`(在Python 3.6及以上版本中)等,可以将常规函数转换为异步协程异步编程的一些关键概念包括: - **事件循环**:协调和调度所有协程的运行。 - **异步I/O**:通过非阻塞I/O,允许程序在等待I/O操作完成时继续执行其他任务。 - **回调和生成器**:早期的异步编程可能使用这些技术,但现代Python更倾向于使用async/await和Task。 如果你对异步编程有深入的兴趣,可能会问到: 1. 异步编程如何提高程序性能? 2. Python中如何正确地管理异步任务的执行顺序? 3. 异步编程中的“回调地狱”是什么,如何避免?

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值