0

0

优化Asyncio嵌套函数调度:使用生产者-消费者模式实现并发流处理

心靈之曲

心靈之曲

发布时间:2025-11-29 10:52:28

|

542人浏览过

|

来源于php中文网

原创

优化asyncio嵌套函数调度:使用生产者-消费者模式实现并发流处理

本文深入探讨了在Python asyncio中调度嵌套异步函数时遇到的并发挑战。通过分析传统`await`操作的阻塞特性,揭示了其在复杂流处理场景中的局限性。文章提出并详细阐述了基于`asyncio.Queue`和`asyncio.Event`的生产者-消费者模式,作为实现任务间解耦和真正并发执行的有效策略,从而显著提升异步应用的响应性和效率。

在异步编程中,我们经常需要处理数据流,其中一个任务负责生成数据,另一个任务负责处理数据。Python的asyncio库提供了强大的工具来构建并发应用程序,但在调度嵌套异步函数时,如果不正确地理解await关键字的行为,可能会导致程序并非按预期并发执行,而是串行阻塞。

传统await的阻塞特性及其局限性

考虑一个常见的场景:我们有一个字符流生成器,它逐步产生字符;一个句子生成器,它从字符流中收集字符并形成完整的句子;以及一个句子处理器,它对每个句子执行耗时操作。

以下是初始实现的代码结构:

import asyncio

async def stream():
    char_string = "Hi. Hello. Hello."
    for char in char_string:
        await asyncio.sleep(0.1)  # 模拟耗时操作
        print("got char:", char)
        yield char

async def sentences_generator():
    sentence = ""
    async for char in stream():
        sentence += char
        if char in [".", "!", "?"]:
            print("got sentence: ", sentence)
            yield sentence
            sentence = ""

async def process_sentence(sentence: str):
    print("waiting for processing sentence: ", sentence)
    await asyncio.sleep(len(sentence)*0.1) # 模拟耗时操作
    print("sentence processed!")

async def main():
    i = 0
    async for sentence in sentences_generator():
        print("processing sentence: ", i)
        await process_sentence(sentence) # 这里的await是关键
        i += 1

asyncio.run(main())

运行上述代码,其输出大致如下:

got char: H
got char: i
got char: .
got sentence:  Hi.
processing sentence:  0
waiting for processing sentence:  Hi.
sentence processed!
got char:  
got char: H
got char: e
got char: l
got char: l
got char: o
got char: .
got sentence:   Hello.
processing sentence:  1
waiting for processing sentence:   Hello.
sentence processed!

从输出可以看出,当process_sentence函数被await时,main协程会暂停,直到process_sentence完全执行完毕。这意味着在process_sentence处理第一个句子期间,stream和sentences_generator无法继续生成新的字符和句子。这并非我们期望的并发行为,我们希望在处理一个句子的同时,上游的字符流能够继续生成,从而提高整体吞吐量。

造成这种现象的根本原因在于await关键字的语义。当一个协程await另一个协程时,它会暂停自身的执行,并将控制权交给被await的协程。只有当被await的协程完成或自身也await了其他操作时,控制权才可能回到调用者。因此,在上述例子中,main函数中的await process_sentence(sentence)会完全阻塞main函数,直到当前句子处理完毕,才能继续从sentences_generator中获取下一个句子。

解决方案:基于asyncio.Queue的生产者-消费者模式

为了实现真正的并发,即在process_sentence处理句子的同时,sentences_generator和stream能够继续生成数据,我们可以采用经典的生产者-消费者模式。

在这种模式中:

  1. 生产者(Producer):负责生成数据(例如,sentences_generator生成句子),并将数据放入一个共享队列中。
  2. 消费者(Consumer):负责从共享队列中取出数据(例如,process_sentence处理句子),并独立执行其任务。

asyncio提供了asyncio.Queue来实现这种异步安全的共享队列。此外,为了优雅地处理生产者完成后的消费者关闭问题,我们还可以引入asyncio.Event来发出生产者完成的信号。

Bing图像创建器
Bing图像创建器

必应出品基于DALL·E的AI绘图工具

下载

以下是使用生产者-消费者模式重构后的代码:

import asyncio

async def stream():
    char_string = "Hi. Hello. Thank you." # 更改了字符串以展示更长的流
    for char in char_string:
        await asyncio.sleep(0.1)
        print("got char:", char)
        yield char

async def sentences_generator(q: asyncio.Queue[str], flag: asyncio.Event):
    """
    生产者:从字符流生成句子,并放入队列。
    当字符流结束时,设置Event标志通知消费者。
    """
    sentence = ""
    async for char in stream():
        sentence += char
        if char in [".", "!", "?"]:
            print("got sentence: ", sentence)
            await q.put(sentence) # 将生成的句子放入队列
            sentence = ""
    flag.set() # 生产者完成所有句子的生成,设置Event

async def process_sentence(q: asyncio.Queue[str], flag: asyncio.Event):
    """
    消费者:从队列中取出句子进行处理。
    当队列为空且生产者已完成时,消费者停止。
    """
    global i # 用于计数处理的句子
    while True:
        # 检查是否应该停止:队列为空且生产者已完成
        if q.empty() and flag.is_set():
            break
        try:
            # 尝试从队列获取项,如果队列为空,会等待
            item = await asyncio.wait_for(q.get(), timeout=1.0) # 增加超时,避免无限等待
        except asyncio.TimeoutError:
            # 如果超时且生产者已完成,则退出
            if flag.is_set():
                break
            continue # 否则继续等待

        print("processing sentence: ", i)
        print("waiting for processing sentence: ", item)
        await asyncio.sleep(len(item) * 0.1)
        print("sentence processed!")
        i += 1

async def main():
    global i
    i = 1 # 初始化句子计数器
    event = asyncio.Event() # 用于生产者通知消费者完成
    queue = asyncio.Queue[str]() # 共享队列

    # 创建生产者和消费者任务
    producer_task = sentences_generator(queue, event)
    consumer_task = process_sentence(queue, event)

    # 并发运行生产者和消费者任务
    await asyncio.gather(producer_task, consumer_task)

asyncio.run(main())

代码解析:

  1. sentences_generator (生产者):

    • 接收一个asyncio.Queue实例q和一个asyncio.Event实例flag。
    • 它继续从stream()生成字符并构建句子。
    • 一旦一个完整的句子形成,它不再直接yield句子,而是使用await q.put(sentence)将句子异步地放入队列。
    • 当stream()耗尽所有字符,即生产者完成其所有工作时,它调用flag.set()来通知消费者没有更多的句子会生成。
  2. process_sentence (消费者):

    • 同样接收q和flag。
    • 在一个无限循环中运行,直到满足退出条件。
    • 使用await q.get()异步地从队列中取出句子。如果队列为空,q.get()会暂停当前协程,直到有新的项可用。
    • 为了更健壮地处理消费者退出,我们添加了asyncio.wait_for和超时机制。当队列为空且flag.is_set()为真时(表示生产者已完成且队列中不再有新数据),消费者将退出循环。
    • 取出句子后,它执行模拟的耗时处理await asyncio.sleep(...)。
  3. main 函数:

    • 初始化asyncio.Event和asyncio.Queue。
    • 使用asyncio.gather(producer_task, consumer_task)同时启动生产者和消费者两个独立的协程。asyncio.gather会等待所有传入的协程完成。由于生产者会在完成后设置事件,而消费者会在队列清空且事件设置后退出,因此gather最终会完成。

预期输出(部分):

got char: H
got char: i
got char: .
got sentence:  Hi.
got char:  
got char: H
got char: e
got char: l
got char: l
got char: o
got char: .
got sentence:   Hello.
processing sentence:  1
waiting for processing sentence:  Hi.
got char:  
got char: T
got char: h
got char: a
got char: n
got char: k
got char:  
got char: y
got char: o
got char: u
got char: .
got sentence:   Thank you.
sentence processed!
processing sentence:  2
waiting for processing sentence:   Hello.
sentence processed!
processing sentence:  3
waiting for processing sentence:   Thank you.
sentence processed!

从新的输出中可以看到,当process_sentence正在处理"Hi."时,stream和sentences_generator已经继续生成了"Hello."甚至"Thank you."。这种交错的输出表明生产者和消费者正在并发地工作,显著提高了程序的效率和响应性。

注意事项与总结

  1. asyncio.Queue的重要性: 它是实现任务间安全通信和解耦的关键。生产者将数据放入队列,消费者从队列取出数据,两者无需直接等待对方完成,只需通过队列进行协调。
  2. asyncio.Event的用途: 在生产者-消费者模式中,asyncio.Event常用于信号通知。生产者完成所有工作后,设置Event,消费者在队列为空时检查此Event,以判断是否可以安全退出,避免消费者在生产者已无数据生成后无限期等待。
  3. 优雅地关闭消费者: 确保消费者能够识别生产者完成的信号并优雅地退出,是构建健壮异步应用的重要一环。除了asyncio.Event,也可以考虑使用特殊的“哨兵值”(Sentinel Value)放入队列来指示生产者结束。
  4. asyncio.gather: 用于同时运行多个协程,并等待它们全部完成。它是协调多个独立或半独立任务的强大工具。
  5. 理解并发与并行: asyncio实现的是并发(concurrency),而非真正的并行(parallelism)。这意味着在单个CPU核心上,任务仍然是交替执行的,但通过await的非阻塞特性,可以在等待I/O操作(如asyncio.sleep)时切换到其他任务,从而提高资源利用率。

通过采用生产者-消费者模式并结合asyncio.Queue和asyncio.Event,我们可以有效地管理异步任务间的依赖关系,实现更高效、更具响应性的并发数据流处理。这对于构建复杂的异步系统,如网络服务、数据管道等,是至关重要的技术。

相关专题

更多
python开发工具
python开发工具

php中文网为大家提供各种python开发工具,好的开发工具,可帮助开发者攻克编程学习中的基础障碍,理解每一行源代码在程序执行时在计算机中的过程。php中文网还为大家带来python相关课程以及相关文章等内容,供大家免费下载使用。

758

2023.06.15

python打包成可执行文件
python打包成可执行文件

本专题为大家带来python打包成可执行文件相关的文章,大家可以免费的下载体验。

639

2023.07.20

python能做什么
python能做什么

python能做的有:可用于开发基于控制台的应用程序、多媒体部分开发、用于开发基于Web的应用程序、使用python处理数据、系统编程等等。本专题为大家提供python相关的各种文章、以及下载和课程。

761

2023.07.25

format在python中的用法
format在python中的用法

Python中的format是一种字符串格式化方法,用于将变量或值插入到字符串中的占位符位置。通过format方法,我们可以动态地构建字符串,使其包含不同值。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

618

2023.07.31

python教程
python教程

Python已成为一门网红语言,即使是在非编程开发者当中,也掀起了一股学习的热潮。本专题为大家带来python教程的相关文章,大家可以免费体验学习。

1264

2023.08.03

python环境变量的配置
python环境变量的配置

Python是一种流行的编程语言,被广泛用于软件开发、数据分析和科学计算等领域。在安装Python之后,我们需要配置环境变量,以便在任何位置都能够访问Python的可执行文件。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

548

2023.08.04

python eval
python eval

eval函数是Python中一个非常强大的函数,它可以将字符串作为Python代码进行执行,实现动态编程的效果。然而,由于其潜在的安全风险和性能问题,需要谨慎使用。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

579

2023.08.04

scratch和python区别
scratch和python区别

scratch和python的区别:1、scratch是一种专为初学者设计的图形化编程语言,python是一种文本编程语言;2、scratch使用的是基于积木的编程语法,python采用更加传统的文本编程语法等等。本专题为大家提供scratch和python相关的文章、下载、课程内容,供大家免费下载体验。

708

2023.08.11

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

40

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 2.8万人学习

Django 教程
Django 教程

共28课时 | 3.2万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号