Python单线程+异步协程

1. 概念

单线程+异步协程是一种编程范式,它结合了单线程执行环境和异步编程技术中的协程特性,以实现在单个线程内高效处理多个并发任务。这种组合主要应用于需要处理大量异步I/O操作(如网络请求、文件读写等)且希望避免传统多线程或多进程带来的额外开销(如线程切换、进程通信等)的场景。

2. 异步协程解决的痛点

痛点一:阻塞性I/O操作导致的效率低下

在传统的同步编程模型中,当程序执行到涉及I/O操作(如网络请求、文件读写、数据库查询等)时,如果采用阻塞式API,线程或进程会一直等待I/O操作完成,期间无法执行其他任何任务。这种情况下,即使系统有多个CPU核心,也无法有效利用这些资源,因为只有一个线程或进程在实际工作,其余的则处于等待状态。尤其是在处理大量I/O操作或高并发场景时,这种阻塞性会导致严重的性能瓶颈和资源浪费。

痛点二:多线程/多进程编程的复杂性

为了解决阻塞性I/O问题,开发者往往会采用多线程或多进程编程。虽然这可以实现并行处理,但也会带来新的挑战:

  • 线程安全问题:多线程环境下,多个线程可能同时访问共享数据,如果不采取适当的同步措施(如锁、信号量等),可能导致数据不一致、竞态条件等错误。
  • 上下文切换开销:线程或进程之间的切换需要保存和恢复寄存器状态、内存映射等信息,频繁的上下文切换会消耗CPU资源,影响整体性能。
  • 资源消耗:每个线程或进程都需要一定的系统资源(如栈空间、打开文件句柄数等),过多的线程或进程可能导致资源枯竭。
  • 编程复杂度:编写正确的多线程或多进程程序需要对并发编程有深入理解,包括死锁、活锁、优先级反转等问题,以及如何正确使用同步原语。此外,调试多线程或多进程程序也往往更为困难。

痛点三:回调地狱与代码可读性

早期的异步编程模型广泛使用回调函数来处理异步操作完成后的逻辑。当程序包含多个嵌套的异步操作时,回调函数会层层嵌套,形成所谓的"回调地狱"。这种代码结构既难以阅读和理解,又不易于维护和扩展。

异步协程的解决方案

异步协程正是为了解决以上痛点而提出的:

  1. 非阻塞I/O:异步协程通过非阻塞I/O操作,使得在等待I/O完成时,线程不会被阻塞,而是可以继续执行其他任务。这样,即使在单线程环境下,也可以高效处理多个并发的I/O操作,充分利用CPU资源。
  2. 简化并发编程:异步协程通过async和await关键字,将异步代码组织得接近于同步代码的结构,避免了复杂的回调函数嵌套,显著提升了代码的可读性和可维护性。同时,协程间的通信和同步通常比多线程更为简单直接。
  3. 降低上下文切换开销:由于异步协程在单线程环境下运行,没有线程间的上下文切换,只需要在协程间切换,这种切换通常比线程切换代价小得多。
  4. 资源高效利用:异步协程在一个线程中即可处理大量并发任务,相比多线程或多进程,对系统资源(如内存、文件描述符等)的需求更低。

3. 关键组成

单线程执行环境

  • 单一执行路径:程序在一个线程中按序执行一系列指令,任何时候只有一个指令在执行,形成单一的控制流。
  • 共享内存:由于是单线程,所有的任务(协程)共享同一块内存空间,无需进行进程间或线程间的内存同步。
  • 无锁竞争:在单线程环境中,不存在不同线程同时修改数据引发的竞态条件问题,因此无需使用锁或其他同步机制来保护共享资源。

异步编程

  • 非阻塞I/O:异步编程的核心是避免在等待I/O操作完成时阻塞线程。当发起一个I/O操作(如网络请求)时,程序不会等待该操作完成,而是立即返回并继续执行其他任务。
  • 回调函数 或 Future/Promise:传统的异步编程通常使用回调函数来处理I/O操作完成后的逻辑。现代异步编程框架常引入Future/Promise等抽象,表示异步操作的结果,允许通过注册回调或者使用.then()、.await等方式获取操作完成后的结果。

协程(Coroutine)

  • 轻量级子任务:协程是一种比线程更轻量级的执行单元,可以看作是程序中的一个子任务,拥有自己的局部状态和控制流。
  • 协同调度:协程之间不是通过抢占式调度(如线程调度)来决定执行顺序,而是通过协作的方式主动交出控制权。当一个协程遇到需要等待的异步操作时,它会挂起自身执行,让出CPU给其他协程。
  • async/await语法:在Python中,协程通过async def定义,内部使用await关键字来暂停协程执行并等待异步操作完成。这使得协程代码看起来像同步代码,易于理解和维护。

事件循环(Event Loop)

  • 调度中心:单线程+异步协程模型中,有一个核心组件------事件循环,负责管理所有协程的执行。事件循环监控所有的异步操作,当某个操作完成时,它将对应的协程从挂起状态恢复执行。
  • 任务队列:事件循环维护一个任务队列,存放待执行或已挂起的协程。每当一个协程挂起等待异步操作时,事件循环会切换到队列中的下一个可执行协程。

4. 工作原理

在单线程+异步协程的程序中:

  1. 启动协程:程序初始化时,创建并启动协程,将它们提交给事件循环。
  2. 调度执行:事件循环按顺序或优先级从任务队列中取出协程开始执行。当协程遇到await某个异步操作时,它会暂停执行并返回控制权给事件循环。
  3. 处理I/O事件:事件循环监测到异步操作完成(如网络请求返回数据),将相关协程标记为可恢复,并将其放入任务队列。
  4. 恢复执行:事件循环从任务队列中取出已完成异步操作的协程,恢复其执行,直到遇到下一个await或协程结束。

通过这种机制,单线程+异步协程能够在单个线程内高效地处理多个并发任务,特别是在处理大量异步I/O操作时,可以避免线程上下文切换的开销,充分利用单个CPU核心,并保持较高的执行效率和较低的系统资源占用。Python的asyncio库是实现单线程+异步协程编程的一个重要工具。

5. 生活举例理解协程

理解Python的单线程+异步协程可以通过一个生活中的例子来帮助记忆和理解。我们以一个家庭主妇准备一顿晚餐的过程为例:

生活场景:家庭主妇准备晚餐

同步方式(单线程):

假设家庭主妇需要独自准备三道菜:炒菜、炖汤和烤面包。在同步方式下,她必须按照顺序依次完成每道菜的全部步骤,不允许跳过或并行操作。

  1. 炒菜:

    • 洗菜(5分钟)
    • 切菜(3分钟)
    • 炒制(2分钟)
  2. 炖汤:

    • 准备食材(2分钟)
    • 炖煮(30分钟)
  3. 烤面包:

    • 和面(10分钟)
    • 发酵(40分钟)
    • 烘烤(15分钟)

按照这个顺序,家庭主妇总共需要花费约1小时37分钟才能完成所有菜品的制作。

异步协程方式(单线程):

现在,想象家庭主妇具备了"异步"和"协程"的能力:

  1. 炒菜:
    • 洗菜(5分钟)
    • 切菜(挂起,等待3分钟)
    • 炒制(2分钟)
  2. 炖汤:
    • 准备食材(2分钟)
    • 炖煮(启动后挂起,等待30分钟)
  3. 烤面包:
    • 和面(10分钟)
    • 发酵(启动后挂起,等待40分钟)
    • 烘烤(15分钟)

在这个异步协程场景中,家庭主妇可以启动一个任务后立即去执行下一个任务,而不必等待当前任务完全完成。比如,她可以先洗菜,然后开始和面;面团发酵时,她可以准备炖汤的食材,然后启动炖煮;接着去切菜,切完后炒制。当炒菜进行时,炖汤和发酵也在后台进行。最后,当炒菜完成时,面包应该已经发酵完毕,可以放入烤箱烘烤。

其中关于异步协程技术要点和概念

异步意味着任务可以非阻塞地执行,即一个任务在等待IO操作(如炖煮、发酵)时,不会阻碍其他任务的执行。在这个例子中,炖汤和发酵就是典型的IO-bound操作,它们在启动后不需要持续的人工干预,可以"异步"进行。

协程是一种轻量级的线程,可以在单个线程内实现多个任务的协作执行。在Python中,协程通过async和await关键字来定义和使用。协程的特点是可以主动挂起(yield)执行,让出控制权给其他协程,当条件满足时再恢复执行。

异步协程如何工作:

  • 事件循环(Event Loop):相当于家庭主妇的大脑,负责协调各个任务的执行顺序。当一个任务(协程)遇到await表达式时,它会暂停执行并返回控制权给事件循环。
  • Future / Task:代表一个异步操作的结果。当炖汤或发酵这样的异步操作开始时,会创建一个Future对象。当操作完成时,Future对象会被标记为完成,并携带结果。事件循环会监视所有Future的状态变化,当它们完成时,重新调度关联的协程继续执行。
  • 协程(Coroutine):家庭主妇的各项操作(如炒菜、炖汤)被封装成协程函数。在需要等待的步骤,如切菜等待时间、炖煮等待时间、发酵等待时间,使用await关键字挂起协程,让事件循环调度其他任务。

在这个例子中,家庭主妇就像一个事件循环,管理着多个协程(炒菜、炖汤、烤面包)。她可以同时启动多个任务,但每个时刻只专注于一个任务的一部分,当某个任务需要等待时,她会切换到另一个任务。通过这种方式,家庭主妇(单线程)在不到1小时的时间内完成了原本需要1小时37分钟的任务,大大提高了效率。

总结起来,Python的单线程+异步协程是一种在单个线程中通过协同调度多个任务来实现并发执行的技术,特别适用于处理大量IO-bound操作。其核心概念包括事件循环、Future/Task和协程,它们共同协作,使得程序在执行过程中能够有效地利用等待时间,避免阻塞,从而提升整体性能。

6. asyncio 模拟家庭主妇准备晚餐过程

python 复制代码
import asyncio

async def wash_vegetables(duration):
    print("开始洗菜,预计耗时{}分钟...".format(duration))
    await asyncio.sleep(duration)
    print("菜已洗净。")

async def chop_vegetables(duration):
    print("开始切菜,预计耗时{}分钟...".format(duration))
    await asyncio.sleep(duration)
    print("菜已切好。")

async def fry_vegetables():
    print("开始炒菜...")
    # 假设炒菜耗时2分钟,此处未显式模拟
    print("菜已炒好。")

async def prepare_soup(duration):
    print("准备汤料,预计耗时{}分钟...".format(duration))
    await asyncio.sleep(duration)
    print("汤料准备完毕。")

async def cook_soup(duration):
    print("开始煮汤,预计耗时{}分钟...".format(duration))
    await asyncio.sleep(duration)
    print("汤已煮好。")

async def knead_dough(duration):
    print("开始揉面,预计耗时{}分钟...".format(duration))
    await asyncio.sleep(duration)
    print("面已揉好。")

async def proof_dough(duration):
    print("开始发酵面团,预计耗时{}分钟...".format(duration))
    await asyncio.sleep(duration)
    print("面团已发酵完成。")

async def bake_bread(duration):
    print("开始烤面包,预计耗时{}分钟...".format(duration))
    await asyncio.sleep(duration)
    print("面包已烤好。")

async def prepare_dinner():
    await asyncio.gather(
        wash_vegetables(5),
        knead_dough(10),
        prepare_soup(2),
    )

    await asyncio.gather(
        chop_vegetables(3),
        proof_dough(40),
        cook_soup(30),
    )

    await fry_vegetables()
    await bake_bread(15)

asyncio.run(prepare_dinner())

7. asyncio 常用 API

asyncio 是 Python 标准库中用于实现异步 I/O 和并发编程的重要模块。结合协程概念,以下是 asyncio 中常用 API 的详细解释:

  1. async 关键字

async 用于定义一个协程函数。协程函数本质上是一个生成器函数,但它使用 await 关键字来暂停执行并等待异步操作。定义协程函数时,在 def 关键字前加上 async:

python 复制代码
async def my_coroutine():
    # 异步代码
    pass
  1. await 关键字

await 用于暂停当前协程的执行,并等待一个 awaitable 对象(如另一个协程、Future、Task 或 asyncio.sleep() 等)的结果。当等待的对象完成时,协程将恢复执行:

python 复制代码
async def my_coroutine():
    response = await fetch_data_from_server()  # 暂停直到获取到数据
    process(response)  # 数据获取完毕后继续执行
  1. asyncio.run()

asyncio.run() 是一个顶级入口函数,用于运行一个协程,并等待其完成。它会自动创建一个事件循环,将协程提交给该事件循环执行,最后关闭事件循环。这是最简单的启动协程的方式

python 复制代码
async def main():
    # 异步代码
    pass

asyncio.run(main())
  1. asyncio.create_task()

asyncio.create_task() 用于将一个协程包装成一个 Task 对象并提交到当前事件循环。Task 代表一个可以并发执行的任务。创建 Task 后并不意味着立即执行,而是由事件循环调度执行:

python 复制代码
async def task1():
    pass

async def task2():
    pass

async def main():
    task1_ = asyncio.create_task(task1())
    task2_ = asyncio.create_task(task2())

    # 可以添加更多异步操作,或使用 await 等待任务完成
    await task1_

asyncio.run(main())
  1. asyncio.gather()

asyncio.gather(*aws, return_exceptions=False) 用于并发执行多个协程,并等待所有协程完成。返回一个包含所有协程结果的列表(如果 return_exceptions=True,则包含异常)。这对于并行处理多个独立任务非常有用:

python 复制代码
async def job1():
    pass

async def job2():
    pass

async def main():
    results = await asyncio.gather(job1(), job2())
    print(results)

asyncio.run(main())
  1. asyncio.sleep()

asyncio.sleep(delay) 是一个模拟异步延迟的实用函数,用于在协程中暂停执行指定秒数。它返回一个 Future 对象,可以被 await:

python 复制代码
async def my_coroutine():
    await asyncio.sleep(2)  # 暂停执行2秒
    print("Resuming after sleep.")
  1. asyncio.Event

asyncio.Event 是一个低级别的同步原语,用于在协程之间传递信号。协程可以等待 (await) 事件的发生,而其他协程可以设置 (set()) 或清除 (clear()) 事件:

python 复制代码
async def waiter(event):
    await event.wait()  # 暂停直到事件被设置

async def setter(event):
    do_something()
    event.set()  # 设置事件,唤醒等待的协程

async def main():
    event = asyncio.Event()
    asyncio.create_task(waiter(event))
    asyncio.create_task(setter(event))

asyncio.run(main())
  1. asyncio.Queue

asyncio.Queue(maxsize=0) 提供了一个线程安全的异步队列,用于在协程之间交换数据。协程可以异步地将数据放入队列 (put_nowait() 或 put()),或从队列取出数据 (get_nowait() 或 get()):

python 复制代码
async def producer(queue):
    for item in generate_items():
        await queue.put(item)

async def consumer(queue):
    while True:
        item = await queue.get()
        process_item(item)
        queue.task_done()

async def main():
    queue = asyncio.Queue()
    asyncio.create_task(producer(queue))
    asyncio.create_task(consumer(queue))

    # 等待所有任务完成(假设 consumer 会消费完所有任务)
    await queue.join()

asyncio.run(main())
  1. 事件循环 (asyncio.get_event_loop())

事件循环是 asyncio 的核心组件,负责调度协程的执行。通过 asyncio.get_event_loop() 可以获取当前线程的事件循环实例。虽然通常使用 asyncio.run() 或 asyncio.create_task() 不直接接触事件循环,但在某些高级用法中可能需要直接操作:

python 复制代码
loop = asyncio.get_event_loop()
task = loop.create_task(my_coroutine())
loop.run_until_complete(task)
loop.close()
相关推荐
waterHBO29 分钟前
python 爬虫 selenium 笔记
爬虫·python·selenium
编程零零七1 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
AIAdvocate3 小时前
Pandas_数据结构详解
数据结构·python·pandas
小言从不摸鱼3 小时前
【AI大模型】ChatGPT模型原理介绍(下)
人工智能·python·深度学习·机器学习·自然语言处理·chatgpt
FreakStudio5 小时前
全网最适合入门的面向对象编程教程:50 Python函数方法与接口-接口和抽象基类
python·嵌入式·面向对象·电子diy
redcocal7 小时前
地平线秋招
python·嵌入式硬件·算法·fpga开发·求职招聘
artificiali7 小时前
Anaconda配置pytorch的基本操作
人工智能·pytorch·python
RaidenQ7 小时前
2024.9.13 Python与图像处理新国大EE5731课程大作业,索贝尔算子计算边缘,高斯核模糊边缘,Haar小波计算边缘
图像处理·python·算法·课程设计
花生了什么树~.8 小时前
python基础知识(六)--字典遍历、公共运算符、公共方法、函数、变量分类、参数分类、拆包、引用
开发语言·python
Trouvaille ~8 小时前
【Python篇】深度探索NumPy(下篇):从科学计算到机器学习的高效实战技巧
图像处理·python·机器学习·numpy·信号处理·时间序列分析·科学计算