Python基础知识之并发相关-2

三 Python协程

3.1 题目

写一个async def produce()往list追加 + async.Event.set(),一个async def consume()循环await event.wait() -> clear -> 取空列表,跑起来观察行为;然后故意把顺序改成"先取再clear",看是否丢消息

3.2 参考答案

python 复制代码
import asyncio

# """
# asyncio.Event: 生产者消费者与丢消息
# 实验目的: 理解asyncio.Event的通知机制,以及为什么Event不能直接当作消息队列使用
# """


async def main():
    queue = []
    event = asyncio.Event()

    async def producer():
        for i in range(5):
            print(f"Producer: Producing item {i}")
            queue.append(i)
            event.set() # 通知消费者有新消息
            await asyncio.sleep(0.1) # 模拟生产时间

    async def consumer():
        result = []
        while len(result) < 5:
            await event.wait() # 等待生产者通知
            event.clear() # 清除事件状态,以便下次等待
            batch = queue[:]
            queue.clear()

            for item in batch:
                print(f"Consumer: Consuming item: {item}")
                result.append(item)
            await asyncio.sleep(3) # 模拟消费时间

        return result

    await asyncio.gather(producer(), consumer())


asyncio.run(main())
 

运行结果:

3.3 执行顺序

关键操作:

python 复制代码
await event.wait()
event.clear()

batch = queue[:]
queue.clear()
操作 作用
event.set() 设置事件,唤醒等待者
event.wait() 事件未设置时挂起,设置后继续
event.clear() 重置事件,恢复未触发状态

这里有个重要概念:Event是状态通知,不是消息队列

连续调用多次set(),并不会累计通知次数

实际工程中:

  • 需要传递消息:优先使用asyncio.Queue
  • 只需要通知状态变化:使用asyncio.Event
  • 需要保证通知和数据状态一致:设计同步协议,不能简单依赖Event

3.3.1 先取在clear

python 复制代码
import asyncio
# """
# asyncio.Event: 生产者消费者与丢消息
# 实验目的: 理解asyncio.Event的通知机制,以及为什么Event不能直接当作消息队列使用
# """

async def main():
    queue = []
    event = asyncio.Event()

    async def producer():
        for i in range(3):
            print(f"Producer: Producing item {i}")
            queue.append(i)
            event.set() # 通知消费者有新消息
            await asyncio.sleep(0.1) # 模拟生产时间

    async def consumer():
        result = []
        while len(result) < 3:
            await event.wait() # 等待生产者通知
            event.clear() # 清除事件状态,以便下次等待
            batch = queue[:]
            queue.clear()

            await asyncio.sleep(0)
            event.clear()


            for item in batch:
                print(f"Consumer: Consuming item: {item}")
                result.append(item)
            await asyncio.sleep(0.1) # 模拟消费时间

        return result

    await asyncio.gather(producer(), consumer())

asyncio.run(main())

执行结果:

结论:

可能会暴露丢失唤醒的问题,但在以上代码不会暴露

注意:CPython的asyncio实现对sleep(0)做了特殊优化,使用一个专门的协程路径,让当前Task暂停,并在后续事件循环迭代中恢复

如何稳定复现丢失唤醒

python 复制代码
import asyncio

async def main():
    queue = []
    event = asyncio.Event()

    async def producer():
        queue.append(0)
        event.set()
        print("Producer: 生产 0")

        await asyncio.sleep(0)

        queue.append(1)
        event.set()
        print("Producer: 生产 1")

    async def consumer():
        await event.wait()
        event.clear()

        batch = queue[:]
        queue.clear()

        print("Consumer: 取走", batch)

        # 让生产者有机会追加第二条消息并 set
        await asyncio.sleep(0)

        # 故意清除生产者刚设置的通知
        event.clear()

        print("Consumer: 清除事件,当前队列:", queue)

        # 再次等待
        await event.wait()

        print("Consumer: 被唤醒")

    await asyncio.gather(producer(), consumer())

asyncio.run(main())

你会发现程序一直无法正常结束。

原因是:

  1. 消费者取走第一条消息。

  2. 消费者执行 sleep(0)。

  3. 生产者追加第二条消息,并调用 event.set()。

  4. 消费者恢复,调用 event.clear()。

  5. 消费者再次等待。

  6. 队列中有第二条消息,但 Event 已经被清除。

3.4 核心结论

既然没有await就不会发生协程切换,那为什么还要使用asyncio.Queue?

因为:asyncio.Event只表示一个布尔状态

python 复制代码
event.set() # True
event.clear() # False

它不记录消息数量,也不负责维护消息与通知之间的一致性

而asyncio.Queue把数据存储和等待通知封装存储到一起

python 复制代码
await queue.put(item)
item = await queue.get()

这就不需要自己维护list+Event的同步关系

要点:在单线程asyncio中,协程切换发生在挂起点,而不是任意Python语句之间。这一点与Go Goroutine的调度模型有明显区别。

四

4.1 题目

写 def gen()和async def gen()各产3个数,分别用for和async for消费,确认两者不能混用

4.2 参考答案

python 复制代码
import asyncio

"""
实验目的:理解同步生成器和异步生成器的区别,以及for和async for的使用边界
"""

def gen():
    for i in range(5):
        yield i

async def agen():
    for i in range(5):
        await asyncio.sleep(0.1)
        yield i

for i in gen():
    print(f"Sync generator: {i}")

async def main():
    async for i in agen():
        print(f"Async generator: {i}")

asyncio.run(main())

运行结果:

gen的结果和async的结果不能混用

python 复制代码
for item in agen():
    pass

会报错:

python 复制代码
TypeError: 'async_generator' object is not iterable

反之:

python 复制代码
async for item in gen():
    pass

也会报错,因为同步生成器不支持异步迭代协议。

记住两个协议:

  • 同步迭代:__iter__ + __next__

  • 异步迭代:__aiter__ + __anext__

4.3 知识点

对比 Generator Async Generator
定义 def + yield async def + yield
消费 for async for
获取下一个元素 next() __anext__()
是否支持 await 不支持 支持
典型场景 同步迭代 异步 IO 流

4.4 异步IO流

可以理解为:数据不是一次性全部返回,而是随着IO操作的进行,分批、持续的异步产生或接收,程序可以在等待下一批数据时执行其他任务

关键词:

  • IO:输入输出,例如网络请求、文件读取、Socket通信
  • 流(Stream):数据连续产生或传输,不一定一次性获取完整
  • 异步(Async):等待数据期间,当前协程可以挂起,让事件循环调度其他协程

4.5 实现异步IO流

python 复制代码
import asyncio

async def data_stream():
    for i in range(3):
        await asyncio.sleep(1)
        yield f"data-{i}"


async def main():
    async for item in data_stream():
        print(item)


asyncio.run(main())

实际开发中的应用场景:

  • HTTP Streaming,例如大文件下载、流式API
  • LLM流式输出,例如ChatGPT的逐字输出,常使用SSE
  • WebSocket,例如在线聊天、实时状态推送
相关推荐
用户69371750013841 小时前
AI 能写代码,但工程架构还是应该由人来定
前端·后端
第九序言T1 小时前
Python 数据处理进阶(二十二)用 Pandas 做一次完整的用户行为分析
开发语言·python·数据分析·pandas
云和数据.ChenGuang1 小时前
再论深拷贝和浅拷贝
开发语言·人工智能·python·1024程序员节
Hilaku1 小时前
前端的技术判断力是怎么练出来的?
前端·javascript·程序员
悠仁さん1 小时前
【C++】set map 使用
java·前端·c++
用户093671755411 小时前
用 4500 行 TypeScript 复刻了童年方格纸上的海战棋
前端·vue.js
CCIE-Yasuo1 小时前
网络安全-学习web开发-基础概念、常用标签
前端·学习
第九序言T1 小时前
Python 数据处理进阶(二十一)相关性与分组对比:数据里藏着什么关系
大数据·人工智能·python·数据分析·pandas
凉介川1 小时前
给 DeepSeek Harness 写插件:按扩展名接管 Windows 右键打开
前端