从两次顺序等待,改成两份任务
上一节第一份工作结束后才创建第二份。这次先创建两个 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
将输出接回执行过程:
- 10 被直接进入,先打印
start 10。 - 10 执行到内部的
await asyncio.sleep(0),让出执行机会,11 开始并打印start 11。 - 11 也执行到内部的
await asyncio.sleep(0)并让出,原来的执行链恢复,10 打印end 10并返回。 - 调用方这时才走到
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_groupslime/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 才让它们都有机会推进。
本节对应的全部源码位置
下面把同类用法在 Slime 中的每个扫描位置接回本节讲解;点击文件名可查看完整源码快照。重复出现的 API 共用解释,具体输入与清理由所在函数决定。
展开本节 108 条源码记录
| 记录 / 讲解 | 源码位置 | 原代码 |
|---|---|---|
| occ-000150 | examples/coding_agent_rl/swe.py:466 _grade_swebench | await asyncio.gather( |
| occ-000167 | examples/multi_agent/agent_system.py:3 <module> | import time |
| occ-000172 | examples/multi_agent/agent_system.py:103 Agent.run | time.sleep(1) |
| occ-000185 | examples/multi_agent/agent_system.py:222 run_agent_system | results = await asyncio.gather(*tasks, return_exceptions=True) |
| occ-000188 | examples/multi_agent/agent_system.py:244 run_agent_system | rewrited_solutions_raw = await asyncio.gather(*tasks, return_exceptions=True) |
| occ-000250 | examples/search-r1/google_search_server.py:70 fetch_all | return await asyncio.gather(*tasks) |
| occ-000362 | slime/agent/adapters/common.py:157 BaseAdapter.__init__ | self.inflight: dict[str, set[asyncio.Task]] = {} |
| occ-000371 | slime/agent/adapters/common.py:237 BaseAdapter.shutdown_session._drain | await asyncio.gather(*pending, return_exceptions=True) |
| occ-000423 | slime/agent/adapters/common.py:547 _abort_sglang_request | await asyncio.gather(*(abort_worker(url) for url in worker_urls)) |
| occ-000570 | slime/backends/megatron_utils/server/megatron_server.py:4 <module> | import time |
| occ-000589 | slime/backends/megatron_utils/server/megatron_server.py:315 _run_stats_printer | time.sleep(interval) |
| occ-000641 | slime/backends/megatron_utils/server/megatron_server.py:689 run_megatron_dp_models_loop_worker | time.sleep(0.02) |
| occ-000656 | slime/backends/megatron_utils/update_weight/update_weight_from_distributed.py:4 <module> | import time |
| occ-000659 | slime/backends/megatron_utils/update_weight/update_weight_from_distributed.py:260 UpdateWeightFromDistributed._update_bucket_weights_from_distributed | time.sleep(0.1) |
| occ-000663 | slime/backends/sglang_utils/deployment.py:6 <module> | import time |
| occ-000666 | slime/backends/sglang_utils/deployment.py:59 _start_router | time.sleep(3) |
| occ-000679 | slime/backends/sglang_utils/server_control.py:57 get_live_router_workers | workers = await asyncio.gather(*(check(worker) for worker in response.json()["workers"])) |
| occ-000708 | slime/backends/sglang_utils/server_control.py:245 abort_servers_until_idle | results = await asyncio.gather( |
| occ-000712 | slime/backends/sglang_utils/sglang_engine.py:6 <module> | import time |
| occ-000721 | slime/backends/sglang_utils/sglang_engine.py:95 _wait_server_healthy | time.sleep(2) |
| occ-000725 | slime/backends/sglang_utils/sglang_engine.py:292 SGLangEngine.flush_cache | time.sleep(1) |
| occ-000726 | slime/backends/sglang_utils/sglang_engine.py:297 SGLangEngine.flush_cache | time.sleep(1) |
| occ-000748 | slime/data/queue_data_source.py:17 <module> | import time |
| occ-000806 | slime/data/queue_data_source.py:1093 QueueReader.get_samples | time.sleep(0.05) |
| occ-000839 | slime/data/transport.py:377 run_rollout_io | pending = asyncio.create_task(asyncio.to_thread(function, *values, **kwargs)) |
| occ-000897 | slime/ray/actor_group.py:4 <module> | import time |
| occ-000899 | slime/ray/actor_group.py:203 RayTrainGroup.release | time.sleep(5) |
| occ-000943 | slime/rollout/fully_async_distributed.py:292 _GenerationActor._start | self.producer = asyncio.create_task(self._produce()) |
| occ-000954 | slime/rollout/fully_async_distributed.py:306 _GenerationActor._next | take = asyncio.create_task(self.outputs.get()) |
| occ-000966 | slime/rollout/fully_async_distributed.py:317 _GenerationActor._next | await asyncio.gather(take, return_exceptions=True) |
| occ-000973 | slime/rollout/fully_async_distributed.py:334 _GenerationActor._produce.<comprehension> | tasks.extend(asyncio.create_task(generate_slot(group)) for group in groups) |
| occ-000977 | slime/rollout/fully_async_distributed.py:338 _GenerationActor._produce | await asyncio.gather(*tasks) |
| occ-000980 | slime/rollout/fully_async_distributed.py:342 _GenerationActor._produce | await asyncio.gather(*tasks, return_exceptions=True) |
| occ-000995 | slime/rollout/fully_async_distributed.py:404 _GenerationActor._rebuffer | self._rebuffer_task = asyncio.create_task(self._flush_rebuffers()) |
| occ-001014 | slime/rollout/fully_async_distributed.py:442 _GenerationActor._close | await asyncio.gather(self.producer, return_exceptions=True) |
| occ-001044 | slime/rollout/fully_async_rollout.py:174 AsyncRolloutWorker._loop | active_tasks: set[asyncio.Task] = set() |
| occ-001050 | slime/rollout/fully_async_rollout.py:209 AsyncRolloutWorker._loop | task = asyncio.create_task( |
| occ-001057 | slime/rollout/fully_async_rollout.py:242 AsyncRolloutWorker._make_done_cb | def _cb(done_task: asyncio.Task) -> None: |
| occ-001090 | slime/rollout/rm_hub/__init__.py:109 batched_async_rm | rewards = await asyncio.gather(*tasks) |
| occ-001119 | slime/rollout/sglang_rollout.py:145 GenerateState.submit_generate_tasks | asyncio.create_task( |
| occ-001141 | slime/rollout/sglang_rollout.py:411 generate_and_rm_group | asyncio.create_task(generate_and_rm(args, sample, current_sampling_params, evaluation=evaluation)) |
| occ-001143 | slime/rollout/sglang_rollout.py:414 generate_and_rm_group | group = await asyncio.gather(*tasks) |
| occ-001151 | slime/rollout/sglang_rollout.py:449 abort | await asyncio.gather(*cancellable_tasks, return_exceptions=True) |
| occ-001174 | slime/rollout/sglang_rollout.py:609 eval_rollout | results_list = await asyncio.gather(*coros) |
| occ-001176 | slime/rollout/sglang_rollout.py:712 eval_rollout_single_dataset | asyncio.create_task( |
| occ-001189 | slime/rollout/sleep_rollout.py:2 <module> | import time |
| occ-001190 | slime/rollout/sleep_rollout.py:10 sleep | time.sleep(3600) |
| occ-001252 | slime/utils/http_utils.py:186 _ShardedHTTPTransport.aclose | await asyncio.gather(*(transport.aclose() for transport in self._transports)) |
| occ-001328 | slime_plugins/rollout_buffer/generator/base_generator.py:4 <module> | import time |
| occ-001332 | slime_plugins/rollout_buffer/generator/base_generator.py:86 query_single_turn | sleep(10) |
| occ-001338 | slime_plugins/rollout_buffer/generator/base_generator.py:226 BaseGenerator.run.read_data_into_queue | time.sleep(300) |
| occ-001346 | slime_plugins/rollout_buffer/generator/base_generator.py:280 run_rollout | time.sleep(10) |
| occ-001351 | slime_plugins/rollout_buffer/rollout_buffer_example.py:2 <module> | import time |
| occ-001359 | slime_plugins/rollout_buffer/rollout_buffer_example.py:243 generate_rollout_async | time.sleep(5) |
| occ-001365 | tests/ci/gpu_lock_exec.py:9 <module> | import time |
| occ-001385 | tests/ci/gpu_lock_exec.py:78 _run_command | time.sleep(1) |
| occ-001391 | tests/ci/gpu_lock_exec.py:161 _try_acquire_specific | time.sleep(SLEEP_BACKOFF * random.random()) |
| occ-001395 | tests/ci/gpu_lock_exec.py:198 _try_acquire_count | time.sleep(SLEEP_BACKOFF * random.random()) |
| occ-001633 | tests/test_distributed_rollout.py:10 <module> | import time |
| occ-001661 | tests/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-001687 | tests/test_distributed_rollout.py:735 scheduler_factory.cleanup | await asyncio.gather(*producers, return_exceptions=True) |
| occ-001690 | tests/test_distributed_rollout.py:745 test_scheduler_replaces_rejected_groups_and_bounds_prefetch | time.sleep(0.05) |
| occ-001694 | tests/test_distributed_rollout.py:799 test_async_scheduler_fast_worker_fills_batch_without_waiting_for_slow_worker | time.sleep(0.1) |
| occ-001747 | tests/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-001774 | tests/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-001779 | tests/test_distributed_rollout.py:1834 test_cancelled_rebuffer_waiter_does_not_drop_its_continuation.exercise | await asyncio.gather(*waiters, return_exceptions=True) |
| occ-001808 | tests/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-001825 | tests/test_fully_async_rollout.py:24 <module> | import time |
| occ-001839 | tests/test_fully_async_rollout.py:250 test_loop_backpressure_stops_topping_up_when_queue_is_full | time.sleep(0.02) |
| occ-001844 | tests/test_fully_async_rollout.py:279 test_local_worker_lifecycle_is_scoped_to_source | time.sleep(0.01) |
| occ-001845 | tests/test_fully_async_rollout.py:284 test_local_worker_lifecycle_is_scoped_to_source | time.sleep(0.03) |
| occ-001873 | tests/test_qwen2.5_0.5B_opd_sglang.py:3 <module> | import time |
| occ-001878 | tests/test_qwen2.5_0.5B_opd_sglang.py:73 _launch_teacher_server | time.sleep(5) |
| occ-001887 | tests/test_qwen2.5_0.5B_training_recovery.py:9 <module> | import time |
| occ-001888 | tests/test_qwen2.5_0.5B_training_recovery.py:75 wait_job | time.sleep(10) |
| occ-001891 | tests/test_qwen2.5_0.5B_training_recovery.py:334 execute.block | time.sleep(300) |
| occ-001892 | tests/test_qwen2.5_0.5B_training_recovery.py:340 execute | time.sleep(0.1) |
| occ-001894 | tests/test_qwen2.5_0.5B_training_recovery.py:356 execute | time.sleep(1) |
| occ-001897 | tests/test_qwen3_4B_external_pd.py:23 <module> | import time |
| occ-001907 | tests/test_qwen3_4B_external_pd.py:210 _launch_sglang_server | time.sleep(5) |
| occ-001925 | tests/test_rollout_manager_recovery.py:248 _ResetPeer.reset_weights_update_groups | import time |
| occ-001926 | tests/test_rollout_manager_recovery.py:250 _ResetPeer.reset_weights_update_groups | time.sleep(300) |
| occ-001927 | tests/test_rollout_manager_recovery.py:257 test_serving_reset_retires_blocked_actor_and_keeps_healthy_peer | import time |
| occ-001928 | tests/test_rollout_manager_recovery.py:299 _AttemptManager.detach_training | import time |
| occ-001929 | tests/test_rollout_manager_recovery.py:301 _AttemptManager.detach_training | time.sleep(300) |
| occ-001930 | tests/test_rollout_manager_recovery.py:325 _PDResetPeer.reset_weights_update_groups | import time |
| occ-001931 | tests/test_rollout_manager_recovery.py:329 _PDResetPeer.reset_weights_update_groups | time.sleep(0.01) |
| occ-001933 | tests/test_rollout_manager_recovery.py:373 test_driver_detach_releases_real_trainer_when_manager_cannot_pause | import time |
| occ-001946 | tests/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-001960 | tests/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-002047 | tests/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-002054 | tests/test_server_control.py:281 test_http_transport_preserves_streams_timeouts_and_connection_budget.check | await asyncio.gather(*tasks, return_exceptions=True) |
| occ-002061 | tests/test_straw_fully_async_recovery.py:16 <module> | import time |
| occ-002077 | tests/test_straw_fully_async_recovery.py:159 _run_job | time.sleep(0.05) |
| occ-002084 | tests/test_straw_fully_async_recovery.py:241 _kill_job | time.sleep(0.05) |
| occ-002088 | tests/test_straw_fully_async_recovery.py:270 test_fully_async_sigkill_and_restart_preserves_results_and_prefixes | time.sleep(0.1) |
| occ-002100 | tests/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-002104 | tests/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-002113 | tests/test_straw_r3.py:174 test_cancelled_straw_publication_drains_before_releasing_capacity.run | results = await asyncio.gather(*tasks, return_exceptions=True) |
| occ-002152 | tests/test_streaming_rollout.py:393 test_stream_cancellation_closes_request_and_keeps_prefix.exercise | request_task = asyncio.create_task( |
| occ-002153 | tests/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-002155 | 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-002160 | tests/test_streaming_rollout.py:403 test_stream_cancellation_closes_request_and_keeps_prefix.exercise | return await asyncio.gather(request_task, server_task) |
| occ-002171 | tests/test_streaming_rollout.py:428 test_unrelated_stream_cancellation_propagates.exercise | task = asyncio.create_task( |
| occ-002179 | tests/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-002198 | tests/test_streaming_rollout.py:568 test_generation_pacing_preserves_concurrency_cancellation_and_reuse.exercise.start | return asyncio.create_task( |
| occ-002211 | tests/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-002223 | tests/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:先处理已经完成的工作 · 源码与教程对照 · 全部用法