从两次顺序等待,改成两份任务

上一节第一份工作结束后才创建第二份。这次先创建两个 Task,再等待它们。

import asyncio

async def generate(rollout_id):
    await asyncio.sleep(0)
    return rollout_id

async def exercise():
    tasks = [asyncio.create_task(generate(i)) for i in (10, 11)]
    print("scheduled", len(tasks))
    results = await asyncio.gather(*tasks)
    print("results", results)

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

下载并运行本节 example.py。只使用 Python 标准库;推荐 Python 3.12,最低 Python 3.11。

运行结果:

scheduled 2
results [10, 11]

这里的关键不是多写了一个 await,而是 create_task() 已把两个协程安排给事件循环。程序保留 tasks 引用,最后由 gather(*tasks) 等待整组结果。

gather 的返回列表按传入顺序排列。即使编号 11 先完成,列表里的第一个结果仍对应传入的编号 10。

用三种情况观察任务是否交错

学过 create_task 之后,再把第 2 节的等待放回多任务环境。三种情况共用下面的生成函数;完整实验文件还包含同步等待的版本。

import asyncio
import time

async def generate(rollout_id):
    print("start", rollout_id)
    await asyncio.sleep(0)
    print("end", rollout_id)
    return rollout_id

对比 A:让出了,但没有另一份任务可运行

仍然使用上面的 generate(),把调用处写成:

async def no_other_task():
    await generate(10)
    await generate(11)

结果仍然是:

A: no other task
start 10
end 10
start 11
end 11

这里已经去掉 first = 和 second =,顺序没有变,所以赋值符号不决定等待顺序。

generate(10) 走到 await asyncio.sleep(0) 时,当前任务确实让出了一次。但 generate(11) 所在的第二行还没执行,它还没有被创建、安排。事件循环没有另一份用户任务可运行,就会在当前任务可以继续时恢复它。

当前只有一条执行链:

exercise / no_other_task 这个任务
  └─ await generate(10)
       └─ await asyncio.sleep(0)  ← 整个当前任务让出
  └─ 第一份返回后,才到 await generate(11)

注意:没有其他任务,不代表没有挂起。 本例 sleep(0) 让出后可以很快恢复;如果是 sleep(1),即使没有别的任务,也要等计时条件满足才能继续。

对比 B:同样让出,但 11 已经提前排队

generate() 一行不改,只改调用处:

async def another_task_is_ready():
    task_11 = asyncio.create_task(generate(11))
    await generate(10)
    await task_11

运行这组实验时,asyncio.run(...) 已经启动了 another_task_is_ready()。当前正在执行的协程就是这个调用方;它要沿着函数体一行一行往下走。

为什么先安排了 11,却先执行 10?

看清第一行和第二行做的是不同的事:

当前走到哪一行这一步做了什么当前由谁继续执行
task_11 = asyncio.create_task(generate(11))创建 11 的任务,并把它安排给事件循环;本例默认调度下没有在这一行交出执行机会调用方继续往下走
await generate(10)沿当前执行链直接进入这份新协程,执行到它返回或内部需要暂停为止进入 10,先打印 start 10
10 内部的 await asyncio.sleep(0)当前执行链暂停一次,把执行机会交回事件循环已排队的 11 可以开始

11 先排队,10 却先被当前调用方直接进入。因此“先排队”不代表会抢在当前调用方的下一行之前执行。

这里的两处 await 也要分开看:外层 await generate(10) 先进入 10 的函数体;内部执行到 await asyncio.sleep(0),才出现本例的这次让出。不是每看见一个 await,就立即换成另一个任务。

在本实验的默认事件循环中,输出为:

B: another task is ready
start 10
start 11
end 10
end 11

将输出接回执行过程:

  1. 10 被直接进入,先打印 start 10。
  2. 10 执行到内部的 await asyncio.sleep(0),让出执行机会,11 开始并打印 start 11。
  3. 11 也执行到内部的 await asyncio.sleep(0) 并让出,原来的执行链恢复,10 打印 end 10 并返回。
  4. 调用方这时才走到 await task_11,等待 11 完成;11 恢复后打印 end 11。

所以,await task_11 在这里负责等待结果,不是到这一行才启动 11。 11 已经在 10 暂停期间开始执行。产生这次让出效果的是 await asyncio.sleep(0),不是单独调用 asyncio.sleep()。

对比 C:任务排好了,但同步 sleep 不让出

把生成函数换成下面这一版。它仍由 async def 定义,但内部使用同步的 time.sleep():

async def generate_blocking(rollout_id):
    print("start", rollout_id)
    time.sleep(0.01)
    print("end", rollout_id)
    return rollout_id

调用处仍然提前安排 11:

async def blocking_sleep():
    task_11 = asyncio.create_task(generate_blocking(11))
    await generate_blocking(10)
    await task_11

输出又变成:

C: blocking sleep
start 10
end 10
start 11
end 11

这次 11 已经排队,但 10 中的 time.sleep() 阻塞了事件循环所在的线程,没有把执行机会交还给循环。直到 10 返回,调用方等待 task_11 时,11 才开始。

特别记住:阻塞线程与挂起协程是两回事

time.sleep() 阻塞运行事件循环的线程;await asyncio.sleep() 挂起当前任务,把执行机会交还给事件循环。

即使同样等待 1 秒,两种写法也有这个区别:

写法当前等待期间发生什么同一事件循环中的其他已就绪任务
time.sleep(1)线程被阻塞,不能继续推进事件循环无法趁这段等待运行
await asyncio.sleep(1)当前任务挂起,等待计时条件满足后恢复可以获得执行机会

这里的挂起是暂停,之后还能继续;不是退出协程或执行完函数。对比 C 中的 11 虽然已排队,但 10 的同步休眠阻塞了事件循环线程,所以 11 不能在这段时间开始。

也不是只有 asyncio.sleep() 才能让出:等待网络数据、异步队列等尚未完成的异步操作时,也可能挂起当前任务。本节使用 sleep,是为了把区别看清楚。

还要保留 await:单独调用 asyncio.sleep(1) 只创建协程对象,不会产生上述等待与让出效果。

把三种情况放在一起

情况11 是否已安排成另一任务10 内部是否让出10 返回前,11 是否开始
A:顺序 await没有await asyncio.sleep(0) 让出没有
B:先创建 Task有await asyncio.sleep(0) 让出有
C:同步 sleep有time.sleep() 阻塞线程没有

下载完整 comparison.py,一次运行三种情况。它是独立的 Python 语言实验,不是 Slime 原始源码;不连接外部服务。上面的三组输出已用 Python 3.12 默认事件循环实际核对。

Slime 怎样组织一组样本

在 generate_and_rm_group 中,输入是同一组样本;循环为每个样本安排 generate_and_rm(...);gather 收齐后把整组结果交还调用方。读取时先看组的输入与输出,不必马上进入生成器的业务细节。

展开 Slime 源码:generate_and_rm_group

generate_and_rm_group
slime/rollout/sglang_rollout.py · L404–L416
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码

    tasks = []
    for idx, sample in enumerate(group):
        current_sampling_params = sampling_params.copy()
        if getattr(args, "sglang_enable_deterministic_inference", False):
            seed = state.group_sampling_seeds[idx]
            current_sampling_params["sampling_seed"] = seed
        tasks.append(
            asyncio.create_task(generate_and_rm(args, sample, current_sampling_params, evaluation=evaluation))
        )

    group = await asyncio.gather(*tasks)

    # for the rm that need the whole group, we will do the rm here

一个重要的失败边界

默认的 gather 会把子任务异常传播给等待者,但不能据此推断其他子任务都已经被取消。return_exceptions=True 则把异常作为列表元素返回,需要调用者检查。

第 5 节会单独讲怎样取消并收尾。这里先保留“安排任务的人也应考虑怎样收回任务”的问题。

复述检查

tasks 保存的是 Task,不是最终样本。gather 被等待之后才拿到整组结果。直接 await generate(...) 是等待一份工作,先创建多个 Task 才让它们都有机会推进。

语义核对:创建 Task、gather。

本节对应的全部源码位置

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

展开本节 108 条源码记录
记录 / 讲解源码位置原代码
occ-000150examples/coding_agent_rl/swe.py:466
_grade_swebench
await asyncio.gather(
occ-000167examples/multi_agent/agent_system.py:3
<module>
import time
occ-000172examples/multi_agent/agent_system.py:103
Agent.run
time.sleep(1)
occ-000185examples/multi_agent/agent_system.py:222
run_agent_system
results = await asyncio.gather(*tasks, return_exceptions=True)
occ-000188examples/multi_agent/agent_system.py:244
run_agent_system
rewrited_solutions_raw = await asyncio.gather(*tasks, return_exceptions=True)
occ-000250examples/search-r1/google_search_server.py:70
fetch_all
return await asyncio.gather(*tasks)
occ-000362slime/agent/adapters/common.py:157
BaseAdapter.__init__
self.inflight: dict[str, set[asyncio.Task]] = {}
occ-000371slime/agent/adapters/common.py:237
BaseAdapter.shutdown_session._drain
await asyncio.gather(*pending, return_exceptions=True)
occ-000423slime/agent/adapters/common.py:547
_abort_sglang_request
await asyncio.gather(*(abort_worker(url) for url in worker_urls))
occ-000570slime/backends/megatron_utils/server/megatron_server.py:4
<module>
import time
occ-000589slime/backends/megatron_utils/server/megatron_server.py:315
_run_stats_printer
time.sleep(interval)
occ-000641slime/backends/megatron_utils/server/megatron_server.py:689
run_megatron_dp_models_loop_worker
time.sleep(0.02)
occ-000656slime/backends/megatron_utils/update_weight/update_weight_from_distributed.py:4
<module>
import time
occ-000659slime/backends/megatron_utils/update_weight/update_weight_from_distributed.py:260
UpdateWeightFromDistributed._update_bucket_weights_from_distributed
time.sleep(0.1)
occ-000663slime/backends/sglang_utils/deployment.py:6
<module>
import time
occ-000666slime/backends/sglang_utils/deployment.py:59
_start_router
time.sleep(3)
occ-000679slime/backends/sglang_utils/server_control.py:57
get_live_router_workers
workers = await asyncio.gather(*(check(worker) for worker in response.json()["workers"]))
occ-000708slime/backends/sglang_utils/server_control.py:245
abort_servers_until_idle
results = await asyncio.gather(
occ-000712slime/backends/sglang_utils/sglang_engine.py:6
<module>
import time
occ-000721slime/backends/sglang_utils/sglang_engine.py:95
_wait_server_healthy
time.sleep(2)
occ-000725slime/backends/sglang_utils/sglang_engine.py:292
SGLangEngine.flush_cache
time.sleep(1)
occ-000726slime/backends/sglang_utils/sglang_engine.py:297
SGLangEngine.flush_cache
time.sleep(1)
occ-000748slime/data/queue_data_source.py:17
<module>
import time
occ-000806slime/data/queue_data_source.py:1093
QueueReader.get_samples
time.sleep(0.05)
occ-000839slime/data/transport.py:377
run_rollout_io
pending = asyncio.create_task(asyncio.to_thread(function, *values, **kwargs))
occ-000897slime/ray/actor_group.py:4
<module>
import time
occ-000899slime/ray/actor_group.py:203
RayTrainGroup.release
time.sleep(5)
occ-000943slime/rollout/fully_async_distributed.py:292
_GenerationActor._start
self.producer = asyncio.create_task(self._produce())
occ-000954slime/rollout/fully_async_distributed.py:306
_GenerationActor._next
take = asyncio.create_task(self.outputs.get())
occ-000966slime/rollout/fully_async_distributed.py:317
_GenerationActor._next
await asyncio.gather(take, return_exceptions=True)
occ-000973slime/rollout/fully_async_distributed.py:334
_GenerationActor._produce.<comprehension>
tasks.extend(asyncio.create_task(generate_slot(group)) for group in groups)
occ-000977slime/rollout/fully_async_distributed.py:338
_GenerationActor._produce
await asyncio.gather(*tasks)
occ-000980slime/rollout/fully_async_distributed.py:342
_GenerationActor._produce
await asyncio.gather(*tasks, return_exceptions=True)
occ-000995slime/rollout/fully_async_distributed.py:404
_GenerationActor._rebuffer
self._rebuffer_task = asyncio.create_task(self._flush_rebuffers())
occ-001014slime/rollout/fully_async_distributed.py:442
_GenerationActor._close
await asyncio.gather(self.producer, return_exceptions=True)
occ-001044slime/rollout/fully_async_rollout.py:174
AsyncRolloutWorker._loop
active_tasks: set[asyncio.Task] = set()
occ-001050slime/rollout/fully_async_rollout.py:209
AsyncRolloutWorker._loop
task = asyncio.create_task(
occ-001057slime/rollout/fully_async_rollout.py:242
AsyncRolloutWorker._make_done_cb
def _cb(done_task: asyncio.Task) -> None:
occ-001090slime/rollout/rm_hub/__init__.py:109
batched_async_rm
rewards = await asyncio.gather(*tasks)
occ-001119slime/rollout/sglang_rollout.py:145
GenerateState.submit_generate_tasks
asyncio.create_task(
occ-001141slime/rollout/sglang_rollout.py:411
generate_and_rm_group
asyncio.create_task(generate_and_rm(args, sample, current_sampling_params, evaluation=evaluation))
occ-001143slime/rollout/sglang_rollout.py:414
generate_and_rm_group
group = await asyncio.gather(*tasks)
occ-001151slime/rollout/sglang_rollout.py:449
abort
await asyncio.gather(*cancellable_tasks, return_exceptions=True)
occ-001174slime/rollout/sglang_rollout.py:609
eval_rollout
results_list = await asyncio.gather(*coros)
occ-001176slime/rollout/sglang_rollout.py:712
eval_rollout_single_dataset
asyncio.create_task(
occ-001189slime/rollout/sleep_rollout.py:2
<module>
import time
occ-001190slime/rollout/sleep_rollout.py:10
sleep
time.sleep(3600)
occ-001252slime/utils/http_utils.py:186
_ShardedHTTPTransport.aclose
await asyncio.gather(*(transport.aclose() for transport in self._transports))
occ-001328slime_plugins/rollout_buffer/generator/base_generator.py:4
<module>
import time
occ-001332slime_plugins/rollout_buffer/generator/base_generator.py:86
query_single_turn
sleep(10)
occ-001338slime_plugins/rollout_buffer/generator/base_generator.py:226
BaseGenerator.run.read_data_into_queue
time.sleep(300)
occ-001346slime_plugins/rollout_buffer/generator/base_generator.py:280
run_rollout
time.sleep(10)
occ-001351slime_plugins/rollout_buffer/rollout_buffer_example.py:2
<module>
import time
occ-001359slime_plugins/rollout_buffer/rollout_buffer_example.py:243
generate_rollout_async
time.sleep(5)
occ-001365tests/ci/gpu_lock_exec.py:9
<module>
import time
occ-001385tests/ci/gpu_lock_exec.py:78
_run_command
time.sleep(1)
occ-001391tests/ci/gpu_lock_exec.py:161
_try_acquire_specific
time.sleep(SLEEP_BACKOFF * random.random())
occ-001395tests/ci/gpu_lock_exec.py:198
_try_acquire_count
time.sleep(SLEEP_BACKOFF * random.random())
occ-001633tests/test_distributed_rollout.py:10
<module>
import time
occ-001661tests/test_distributed_rollout.py:613
test_slow_task_acquire_does_not_block_async_generation.exercise
task = asyncio.create_task(source.get_samples_async(1))
occ-001687tests/test_distributed_rollout.py:735
scheduler_factory.cleanup
await asyncio.gather(*producers, return_exceptions=True)
occ-001690tests/test_distributed_rollout.py:745
test_scheduler_replaces_rejected_groups_and_bounds_prefetch
time.sleep(0.05)
occ-001694tests/test_distributed_rollout.py:799
test_async_scheduler_fast_worker_fills_batch_without_waiting_for_slow_worker
time.sleep(0.1)
occ-001747tests/test_distributed_rollout.py:1037
test_worker_refills_locally_and_slow_group_does_not_block_results.exercise
await asyncio.gather(worker.producer, return_exceptions=True)
occ-001774tests/test_distributed_rollout.py:1831
test_cancelled_rebuffer_waiter_does_not_drop_its_continuation.exercise.<comprehension>
waiters = [asyncio.create_task(actor._rebuffer(i)) for i in range(130)]
occ-001779tests/test_distributed_rollout.py:1834
test_cancelled_rebuffer_waiter_does_not_drop_its_continuation.exercise
await asyncio.gather(*waiters, return_exceptions=True)
occ-001808tests/test_distributed_rollout.py:2094
test_queue_fetch_finishing_after_pause_preserves_group_without_new_generation.run
task = asyncio.create_task(worker._generate_group(0))
occ-001825tests/test_fully_async_rollout.py:24
<module>
import time
occ-001839tests/test_fully_async_rollout.py:250
test_loop_backpressure_stops_topping_up_when_queue_is_full
time.sleep(0.02)
occ-001844tests/test_fully_async_rollout.py:279
test_local_worker_lifecycle_is_scoped_to_source
time.sleep(0.01)
occ-001845tests/test_fully_async_rollout.py:284
test_local_worker_lifecycle_is_scoped_to_source
time.sleep(0.03)
occ-001873tests/test_qwen2.5_0.5B_opd_sglang.py:3
<module>
import time
occ-001878tests/test_qwen2.5_0.5B_opd_sglang.py:73
_launch_teacher_server
time.sleep(5)
occ-001887tests/test_qwen2.5_0.5B_training_recovery.py:9
<module>
import time
occ-001888tests/test_qwen2.5_0.5B_training_recovery.py:75
wait_job
time.sleep(10)
occ-001891tests/test_qwen2.5_0.5B_training_recovery.py:334
execute.block
time.sleep(300)
occ-001892tests/test_qwen2.5_0.5B_training_recovery.py:340
execute
time.sleep(0.1)
occ-001894tests/test_qwen2.5_0.5B_training_recovery.py:356
execute
time.sleep(1)
occ-001897tests/test_qwen3_4B_external_pd.py:23
<module>
import time
occ-001907tests/test_qwen3_4B_external_pd.py:210
_launch_sglang_server
time.sleep(5)
occ-001925tests/test_rollout_manager_recovery.py:248
_ResetPeer.reset_weights_update_groups
import time
occ-001926tests/test_rollout_manager_recovery.py:250
_ResetPeer.reset_weights_update_groups
time.sleep(300)
occ-001927tests/test_rollout_manager_recovery.py:257
test_serving_reset_retires_blocked_actor_and_keeps_healthy_peer
import time
occ-001928tests/test_rollout_manager_recovery.py:299
_AttemptManager.detach_training
import time
occ-001929tests/test_rollout_manager_recovery.py:301
_AttemptManager.detach_training
time.sleep(300)
occ-001930tests/test_rollout_manager_recovery.py:325
_PDResetPeer.reset_weights_update_groups
import time
occ-001931tests/test_rollout_manager_recovery.py:329
_PDResetPeer.reset_weights_update_groups
time.sleep(0.01)
occ-001933tests/test_rollout_manager_recovery.py:373
test_driver_detach_releases_real_trainer_when_manager_cannot_pause
import time
occ-001946tests/test_rollout_sample_hooks.py:68
test_concurrent_groups_keep_their_own_rollout_hook_context.exercise
assert await asyncio.gather(generate(10), generate(11)) == [("seen", 10), ("seen", 11)]
occ-001960tests/test_rollout_straw_transport.py:387
test_synchronous_rollout_stores_during_generation_and_preserves_legacy_hook.exercise.submit.<comprehension>
state.pendings.update(asyncio.create_task(generate(index)) for index in (2, 1))
occ-002047tests/test_server_control.py:272
test_http_transport_preserves_streams_timeouts_and_connection_budget.check
await asyncio.gather(*(request(i) for i in range(concurrency + 3)))
occ-002054tests/test_server_control.py:281
test_http_transport_preserves_streams_timeouts_and_connection_budget.check
await asyncio.gather(*tasks, return_exceptions=True)
occ-002061tests/test_straw_fully_async_recovery.py:16
<module>
import time
occ-002077tests/test_straw_fully_async_recovery.py:159
_run_job
time.sleep(0.05)
occ-002084tests/test_straw_fully_async_recovery.py:241
_kill_job
time.sleep(0.05)
occ-002088tests/test_straw_fully_async_recovery.py:270
test_fully_async_sigkill_and_restart_preserves_results_and_prefixes
time.sleep(0.1)
occ-002100tests/test_straw_r3.py:160
test_cancelled_straw_publication_drains_before_releasing_capacity.run
first = asyncio.create_task(rollout_transport.publish_rollout_async(samples[0], args, 0))
occ-002104tests/test_straw_r3.py:165
test_cancelled_straw_publication_drains_before_releasing_capacity.run
second = asyncio.create_task(rollout_transport.publish_rollout_async(samples[1], args, 0))
occ-002113tests/test_straw_r3.py:174
test_cancelled_straw_publication_drains_before_releasing_capacity.run
results = await asyncio.gather(*tasks, return_exceptions=True)
occ-002152tests/test_streaming_rollout.py:393
test_stream_cancellation_closes_request_and_keeps_prefix.exercise
request_task = asyncio.create_task(
occ-002153tests/test_streaming_rollout.py:400
test_stream_cancellation_closes_request_and_keeps_prefix.exercise
server_task = asyncio.create_task(sglang_rollout.generate_and_rm(args, Sample(), {}))
occ-002155tests/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-002160tests/test_streaming_rollout.py:403
test_stream_cancellation_closes_request_and_keeps_prefix.exercise
return await asyncio.gather(request_task, server_task)
occ-002171tests/test_streaming_rollout.py:428
test_unrelated_stream_cancellation_propagates.exercise
task = asyncio.create_task(
occ-002179tests/test_streaming_rollout.py:472
test_partial_abort_buffers_and_resumes_only_aborted_siblings.exercise.<comprehension>
asyncio.create_task(asyncio.sleep(0, result=group)) for group in (mixed_group, [terminal], [empty])
occ-002198tests/test_streaming_rollout.py:568
test_generation_pacing_preserves_concurrency_cancellation_and_reuse.exercise.start
return asyncio.create_task(
occ-002211tests/test_streaming_rollout.py:585
test_generation_pacing_preserves_concurrency_cancellation_and_reuse.exercise
await asyncio.gather(*(task for i, task in enumerate(tasks) if i != 70))
occ-002223tests/test_streaming_rollout.py:611
test_abort_while_waiting_for_generation_pacer_does_not_start_request.exercise
task = asyncio.create_task(sglang_rollout.generate_and_rm(args, Sample(), {}))

专题首页 · 上一节 · 下一节:04 · wait 与 as_completed:先处理已经完成的工作 · 源码与教程对照 · 全部用法