已经有一个任务,为什么还要 Event

前面已经会用 create_task() 启动任务,用 await task 等它结束。现在多一个要求:任务已经开始,但必须等控制方允许,才能继续下一步。例如取样任务已经进入 fetch(),测试需要先执行暂停操作,再允许取样返回。

这里有两个不同的等待:控制方等“已经进入 fetch”,fetch 等“允许返回”。用两个 Event 能明确表达这两次交接,不需要猜测 sleep(0.01) 是否足够。

第一步:把通知看作一个布尔标志

asyncio.Event() 初始保存 False。await event.wait() 在标志为假时挂起当前任务,让事件循环运行其他任务;标志为真时立即返回 True。event.set() 把标志设为真,并唤醒正在等待的任务;event.is_set() 只读取当前标志。它们中的 set() 和 is_set() 不需要 await。Python 3.12 Event 文档

先只看这一小段。group 是我们最终要返回的一组样本;count 和返回的“样本组列表”保持源码接口。

entered, release = asyncio.Event(), asyncio.Event()
group = ["sample"]

async def fetch(count):
    entered.set()              # 告诉控制方:我已经进入取样
    await release.wait()       # 等控制方允许我返回
    return [group][:count]

task = asyncio.create_task(fetch(1))
await entered.wait()
# 到这里,fetch 已经运行到 release.wait(),但还没有返回。
release.set()
result = await task

此段位于异步函数内。entered.set() 后,fetch 接着执行到尚未设置的 release.wait() 才挂起,因此控制方从 entered.wait() 恢复时,能检查一个已知的等待位置。

顺序fetch 任务控制方release 标志
1尚未运行创建任务,然后等 enteredFalse
2entered.set,进入 release.wait可以恢复执行False
3仍在等待执行 release.setTrue
4被安排继续,返回样本组await task 收到返回值True

set() 使等待者能够恢复;它不会直接代替事件循环执行等待者后面的所有代码。要确认任务已经结束,仍然要 await task。

第二步:理解 clear 为什么不是“扣掉一次通知”

release.set() 两次后,标志仍然只是 True。这时连续执行两次 await release.wait(),两次都能立即返回。wait() 不消耗通知,也不自动清零。

release.clear() 把标志重新设成 False,使随后开始的等待需要下一次 set();它不会把已经结束的任务退回去,也不能收回已经唤醒的等待者。因此要像本节示例一样,先等上一轮任务结束,再 clear,才开始下一轮。Event.clear 语义

Event 不保存“来了三份数据”这种数量,也不携带样本。样本仍在 group 中。如果每次通知都必须消费一份独立工作,应回到第 07 节的 Queue;Event 适合表达“已就绪”“允许继续”这样的状态。

回到 Slime:先暂停,再让取样返回

这次沿测试中的 _GenerationActor._generate_group() 进入异步取样。测试先等 entered,确认取样确实在途,再调用 worker._pause(),最后 release.set,让取样结果回来。随后检查:结果被放回缓冲,没有新启动生成请求。Event 的作用是把暂停与返回的相对顺序固定下来。

源码入口:取样与暂停的双向通知

test_queue_fetch_finishing_after_pause_preserves_group_without_new_generation.run
tests/test_distributed_rollout.py · L2071–L2104
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    async def run():
        entered, release = asyncio.Event(), asyncio.Event()
        sample = Sample(tokens=[1, 2], status=Sample.Status.ABORTED)
        sample._queue_lease = {"task_id": "borrowed"}
        group = [sample]
        buffered = []

        async def fetch(count):
            entered.set()
            await release.wait()
            return [group]

        async def rebuffer(value):
            buffered.append(value)

        async def unexpected(*args):
            raise AssertionError("Paused worker admitted a generation request")

        monkeypatch.setattr(sglang_rollout, "generate_and_rm_group", unexpected)
        worker = _GenerationActor.__new__(_GenerationActor)
        worker.running = True
        worker.data_source = SimpleNamespace(get_samples_async=fetch)
        worker._rebuffer = rebuffer
        task = asyncio.create_task(worker._generate_group(0))
        await entered.wait()
        await worker._pause()
        release.set()
        assert await task is None
        assert buffered == [group]
        assert sample._queue_lease == {"task_id": "borrowed"}
        assert sample.tokens == [1, 2]

    asyncio.run(run())

同一版本其他 Event 用法也围绕“把测试停在一个明确位置”展开:

源码场景谁设置,谁等待读完要确认什么
慢生成组与其他组并发测试设置 release,第一组生成等待慢组仍被挡住时,其他组能返回;is_set 检查挡板状态
流式取消流发出首块后设置 first_chunk_seen;服务器任务设置 server_started测试等两个条件,再取消;request_closed.is_set 检查清理回调发生
节奏控制指定数量任务启动后设置 first_batch / all_started,生成任务等 finish检查在途数量与取消,再统一放行
中途恢复await asyncio.Event().wait() 没有设置者有意保持在途,等待外部取消或进程终止,不是业务中会自行结束的等待
样本存储顺序存储后通过 loop.call_soon_threadsafe(second.set) 通知Event 由事件循环线程设置;线程间交接不能直接当作 Event 线程安全
上述场景的固定版本位置

索引中 18 处 asyncio.Event() 构造均在测试文件。该版本没有直接调用 asyncio.Event.clear();本节补充它,是为了完整说明复用状态的语义。测试还在学习包摘录之外使用了 wait_for(event.wait(), timeout):Event 本身不接受 timeout,超时管理交给前面学过的异步等待工具。

有通知还不够:共享状态需要一起修改

Event 可以放行多个任务。若两个请求都准备更新同一份状态,还需要 asyncio.Lock():await lock.acquire() 等到锁可用并取得它;lock.release() 释放锁;lock.locked() 查询当前是否已被取得。查询结果不是取得锁的保证,不要用“先查 locked 再修改状态”替代 acquire。Python 3.12 Lock 文档

通常直接写 async with lock,让异常或取消退出代码块时也能释放。它对应下面的控制结构:

await update_lock.acquire()
try:
    # 读取并修改需要保持一致的共享状态
    ...
finally:
    update_lock.release()

同一把异步锁已有多个等待者时,先开始等待 acquire 的任务先获得锁。对未锁定的锁调用 release() 会抛出 RuntimeError。asyncio.Lock 也不提供同一任务重入的承诺:持有它时再次 acquire 会把自己挡住;下一节的 threading.RLock 是不同的机制。

Slime 的 HTTP 更新请求先进入 update_from_disk(),在锁内检查 in_progress、模型路径与已有 Future:同路径请求共用 Future,不同路径请求返回冲突;新更新则预留状态。之后离开锁再做耗时更新,最后重新取得锁,设置结果并清理状态。这把锁保护的是短暂的状态交接,没有覆盖整个磁盘更新过程。

源码入口:检查状态、预留更新与最终清理

_build_http_app / update_from_disk
slime/backends/megatron_utils/server/megatron_server.py · L422–L468
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

def _build_http_app(sample_manager, args, update_from_disk_fn=None):
    app = web.Application(client_max_size=64 * 1024 * 1024)
    update_state = {
        "in_progress": False,
        "updating_model_path": None,
        "update_future": None,
    }
    update_lock = asyncio.Lock()

    async def detect(_request: web.Request) -> web.Response:
        return web.json_response({"server_type": "megatron_server"})

    async def healthz(_request: web.Request) -> web.Response:
        return web.json_response({"ok": True})

    async def info(_request: web.Request) -> web.Response:
        return web.json_response({"args": _args_to_dict(args)})

    async def get_loads(_request: web.Request) -> web.Response:
        return web.json_response(await _ray_get(sample_manager.get_loads.remote()))

    async def update_from_disk(request: web.Request) -> web.Response:
        if update_from_disk_fn is None:
            return _json_error("update_from_disk is not available during warmup", 503)

        payload = await _read_json_payload(request)
        model_path = _get_update_model_path(payload)
        if model_path is None:
            return _json_error("missing model_path", 400)

        async with update_lock:
            if getattr(args, "load", None) == model_path:
                return web.json_response({"ok": True, "model_path": model_path, "skipped": True})

            if update_state["in_progress"]:
                if update_state["updating_model_path"] == model_path and update_state["update_future"] is not None:
                    update_future = update_state["update_future"]
                    coalesced = True
                else:
                    updating_model_path = update_state["updating_model_path"]
                    return _json_error(f"update_from_disk is already in progress for {updating_model_path}", 409)
            else:
                update_future = asyncio.get_running_loop().create_future()
                update_state["in_progress"] = True
                update_state["updating_model_path"] = model_path
                update_state["update_future"] = update_future
                coalesced = False

update_from_disk: finally
slime/backends/megatron_utils/server/megatron_server.py · L484–L515
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

            after_loads = await _ray_get(sample_manager.get_loads.remote())
        except TimeoutError as e:
            error = {"ok": False, "status": 503, "error": str(e)}
        except Exception as e:
            error = {"ok": False, "status": 500, "error": f"update_from_disk failed: {e}"}
        finally:
            if error is None:
                result = {
                    "ok": True,
                    "model_path": model_path,
                    "before_loads": before_loads,
                    "after_loads": after_loads,
                    "update_result": update_result,
                }
                # Reflect the freshly loaded checkpoint in /info. The actors restore
                # their own args after loading, so only the server-side copy needs to be
                # kept in sync here.
                args.load = model_path
                args.ref_load = model_path

            async with update_lock:
                if result is not None:
                    update_future.set_result(result)
                else:
                    update_future.set_result(error)
                update_state["in_progress"] = False
                update_state["updating_model_path"] = None
                update_state["update_future"] = None

        if result is not None:
            return web.json_response(result)
        return _json_error(error["error"], error["status"])

即使事件循环通常只有一个线程,也需要检查 await 边界:一次逻辑操作如果跨越挂起点,别的任务可能观察或改变共享状态。反过来,也不要把耗时同步函数放到锁内,以为加锁能让它变成异步。

运行一个固定顺序的实验

下载 example.py,运行 python3 -W error example.py。只使用标准库,推荐 Python 3.12,最低 Python 3.11。

前半段沿用 Slime 测试的 entered/release 和 fetch(count),把实际取样替换成内存样本。后半段是独立的 Lock 语义实验:保留 update_from_disk、model_path 与 updating_model_path 这些对应关系,但输入改为字典,省略 HTTP、合并请求和磁盘更新,不能当作原实现。

initial False
waiting True
set True
received [['sample']]
wait again True
cleared False
waiting again True
lock held True
update waiting True
updated checkpoint-2
lock released False
展开完整可运行示例
"""Event notifications and task mutual exclusion; Python 3.11+."""

import asyncio


async def main():
    # Keep the entered/release handshake and fetch(count) from Slime's test.
    entered, release = asyncio.Event(), asyncio.Event()
    group = ["sample"]

    async def fetch(count):
        entered.set()
        await release.wait()
        return [group][:count]

    print("initial", release.is_set())
    task = asyncio.create_task(fetch(1))
    await entered.wait()
    print("waiting", not task.done())
    release.set()
    release.set()  # Two notifications still leave only one True flag.
    print("set", release.is_set())
    print("received", await task)
    print("wait again", await release.wait())

    release.clear()
    entered.clear()
    print("cleared", release.is_set())
    task = asyncio.create_task(fetch(1))
    await entered.wait()
    print("waiting again", not task.done())
    release.set()
    await task

    # A separate API experiment: hold a lock while a known task requests it.
    update_lock = asyncio.Lock()
    requested = asyncio.Event()
    update_state = {"updating_model_path": None}

    async def update_from_disk(request):
        requested.set()
        async with update_lock:
            update_state["updating_model_path"] = request["model_path"]
        return update_state["updating_model_path"]

    await update_lock.acquire()
    try:
        task = asyncio.create_task(update_from_disk({"model_path": "checkpoint-2"}))
        await requested.wait()
        print("lock held", update_lock.locked())
        print("update waiting", not task.done())
    finally:
        update_lock.release()
    print("updated", await task)
    print("lock released", update_lock.locked())


if __name__ == "__main__":
    asyncio.run(main())

这里的顺序由 Event 握手和显式持锁确定,没有依赖机器速度。Event 与异步 Lock 都用于同一事件循环中的任务,不是 OS 线程间同步对象。

复述检查
  1. release.set 两次,再 wait 两次,第二次会等待吗?不会,标志没有被消费。
  2. clear 能撤回已经返回的 fetch 吗?不能,它改变的是后续等待条件。
  3. set 之后怎样确认工作完成?await 对应任务。
  4. locked 返回 False 后能直接写共享状态吗?应先 acquire;观察状态不等于占有锁。
书籍核对与本节判断

书上怎么说: 无直接对应条目。本地《Fluent Python 第 2 版》标识符索引及中英全文检索 asyncio.Event、asyncio.Lock、Condition、RLock,没有覆盖本节这组异步同步原语的说明;不能把第 19 章的 threading.Event 直接当作 asyncio.Event。

我的判断: 上面的教学顺序来自实际 Slime 测试和本节实验;方法语义按 Python 3.12 官方文档核对。书籍基于 Python 3.10,运行验证环境为 Python 3.12.3。

本节对应的全部源码位置

下面把同类用法在 Slime 中的每个扫描位置接回本节讲解;点击文件名可查看完整源码快照。重复出现的 API 共用解释,具体输入与清理由所在函数决定。

展开本节 36 条源码记录
记录 / 讲解源码位置原代码
occ-000604slime/backends/megatron_utils/server/megatron_server.py:429
_build_http_app
update_lock = asyncio.Lock()
occ-001720tests/test_distributed_rollout.py:977
test_worker_refills_locally_and_slow_group_does_not_block_results
release = asyncio.Event()
occ-001723
有接收者线索
tests/test_distributed_rollout.py:990
test_worker_refills_locally_and_slow_group_does_not_block_results.generate_group
await release.wait()
occ-001733
有接收者线索
tests/test_distributed_rollout.py:1013
test_worker_refills_locally_and_slow_group_does_not_block_results.exercise
assert not release.is_set()
occ-001741
有接收者线索
tests/test_distributed_rollout.py:1027
test_worker_refills_locally_and_slow_group_does_not_block_results.exercise
release.set()
occ-001800tests/test_distributed_rollout.py:2072
test_queue_fetch_finishing_after_pause_preserves_group_without_new_generation.run
entered, release = asyncio.Event(), asyncio.Event()
occ-001801tests/test_distributed_rollout.py:2072
test_queue_fetch_finishing_after_pause_preserves_group_without_new_generation.run
entered, release = asyncio.Event(), asyncio.Event()
occ-001955tests/test_rollout_straw_transport.py:376
test_synchronous_rollout_stores_during_generation_and_preserves_legacy_hook.exercise
second = asyncio.Event()
occ-001959
有接收者线索
tests/test_rollout_straw_transport.py:381
test_synchronous_rollout_stores_during_generation_and_preserves_legacy_hook.exercise.generate
await second.wait()
occ-002064tests/test_straw_fully_async_recovery.py:49
_generate_with_interrupt
await asyncio.Event().wait()
occ-002065
有接收者线索
tests/test_straw_fully_async_recovery.py:49
_generate_with_interrupt
await asyncio.Event().wait()
occ-002067tests/test_straw_fully_async_recovery.py:52
_generate_with_interrupt
await asyncio.Event().wait()
occ-002068
有接收者线索
tests/test_straw_fully_async_recovery.py:52
_generate_with_interrupt
await asyncio.Event().wait()
occ-002134tests/test_streaming_rollout.py:352
test_stream_cancellation_closes_request_and_keeps_prefix
first_chunk_seen = asyncio.Event()
occ-002135tests/test_streaming_rollout.py:353
test_stream_cancellation_closes_request_and_keeps_prefix
request_closed = asyncio.Event()
occ-002136tests/test_streaming_rollout.py:354
test_stream_cancellation_closes_request_and_keeps_prefix
server_started = asyncio.Event()
occ-002137tests/test_streaming_rollout.py:355
test_stream_cancellation_closes_request_and_keeps_prefix
server_released = asyncio.Event()
occ-002140
有接收者线索
tests/test_streaming_rollout.py:368
test_stream_cancellation_closes_request_and_keeps_prefix.lines
first_chunk_seen.set()
occ-002142tests/test_streaming_rollout.py:369
test_stream_cancellation_closes_request_and_keeps_prefix.lines
await asyncio.Event().wait()
occ-002143
有接收者线索
tests/test_streaming_rollout.py:369
test_stream_cancellation_closes_request_and_keeps_prefix.lines
await asyncio.Event().wait()
occ-002145
有接收者线索
tests/test_streaming_rollout.py:376
test_stream_cancellation_closes_request_and_keeps_prefix.server_generate
server_started.set()
occ-002147
有接收者线索
tests/test_streaming_rollout.py:377
test_stream_cancellation_closes_request_and_keeps_prefix.server_generate
await server_released.wait()
occ-002150
有接收者线索
tests/test_streaming_rollout.py:386
test_stream_cancellation_closes_request_and_keeps_prefix.abort_servers
server_released.set()
occ-002156
有接收者线索
tests/test_streaming_rollout.py:401
test_stream_cancellation_closes_request_and_keeps_prefix.exercise
await asyncio.gather(first_chunk_seen.wait(), server_started.wait())
occ-002157
有接收者线索
tests/test_streaming_rollout.py:401
test_stream_cancellation_closes_request_and_keeps_prefix.exercise
await asyncio.gather(first_chunk_seen.wait(), server_started.wait())
occ-002162
有接收者线索
tests/test_streaming_rollout.py:410
test_stream_cancellation_closes_request_and_keeps_prefix
assert request_closed.is_set()
occ-002163tests/test_streaming_rollout.py:416
test_unrelated_stream_cancellation_propagates
request_started = asyncio.Event()
occ-002165
有接收者线索
tests/test_streaming_rollout.py:420
test_unrelated_stream_cancellation_propagates.lines
request_started.set()
occ-002167tests/test_streaming_rollout.py:421
test_unrelated_stream_cancellation_propagates.lines
await asyncio.Event().wait()
occ-002168
有接收者线索
tests/test_streaming_rollout.py:421
test_unrelated_stream_cancellation_propagates.lines
await asyncio.Event().wait()
occ-002173
有接收者线索
tests/test_streaming_rollout.py:435
test_unrelated_stream_cancellation_propagates.exercise
await request_started.wait()
occ-002189tests/test_streaming_rollout.py:539
test_generation_pacing_preserves_concurrency_cancellation_and_reuse.exercise
first_batch, all_started, finish = asyncio.Event(), asyncio.Event(), asyncio.Event()
occ-002190tests/test_streaming_rollout.py:539
test_generation_pacing_preserves_concurrency_cancellation_and_reuse.exercise
first_batch, all_started, finish = asyncio.Event(), asyncio.Event(), asyncio.Event()
occ-002191tests/test_streaming_rollout.py:539
test_generation_pacing_preserves_concurrency_cancellation_and_reuse.exercise
first_batch, all_started, finish = asyncio.Event(), asyncio.Event(), asyncio.Event()
occ-002215tests/test_streaming_rollout.py:597
test_abort_while_waiting_for_generation_pacer_does_not_start_request.exercise
queued, release = asyncio.Event(), asyncio.Event()
occ-002216tests/test_streaming_rollout.py:597
test_abort_while_waiting_for_generation_pacer_does_not_start_request.exercise
queued, release = asyncio.Event(), asyncio.Event()

专题首页 · 上一节 · 下一节:Condition、RLock 与 Timer · 源码与教程对照 · 全部用法