异步函数里调用同步函数,会发生什么
如果这个同步函数正在读磁盘、等待同步网络请求或做耗时处理,事件循环的线程会被它占住。把外层函数写成 async def,不会自动改变内部同步调用的执行位置。
先用下面的实验比较两种交接方式。为了专注接口,get_samples() 只生成列表;真实 Slime 在同一位置进行阻塞式读取。
import asyncio
from concurrent.futures import ThreadPoolExecutor
def get_samples(num_samples):
return list(range(num_samples))
async def get_samples_async(reader, num_samples):
loop = asyncio.get_running_loop()
return await loop.run_in_executor(reader, get_samples, num_samples)
async def exercise(reader):
print("to_thread", await asyncio.to_thread(get_samples, 3))
print("reader", await get_samples_async(reader, 3))
if __name__ == "__main__":
with ThreadPoolExecutor(max_workers=1) as reader:
asyncio.run(exercise(reader))
下载并运行本节 example.py。只使用 Python 标准库;推荐 Python 3.12,最低 Python 3.11。
运行结果:
to_thread [0, 1, 2]
reader [0, 1, 2]
to_thread() 使用默认线程池执行同步函数。run_in_executor(reader, ...) 把工作交给明确传入的执行器。等待者都可以用 await 拿结果。
Slime 为什么需要专用 reader
QueueReader 创建只有一个工作线程的 reader,再由 get_samples_async() 把同步 get_samples() 交给它。这样可以保持读取顺序,也不让读取直接占据事件循环线程。
展开 Slime 源码:QueueReader.__init__
QueueReader.__init__slime/data/queue_data_source.py · L1034–L1045
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码
self._lock = threading.RLock()
self._reader = ThreadPoolExecutor(max_workers=1, thread_name_prefix="queue-input")
self._stop = threading.Event()
self._heartbeat_error = None
filter_path = getattr(args, "buffer_filter_path", None)
if filter_path is not None:
raise ValueError(
"--buffer-filter-path is not supported by straw; the queue prioritizes ready groups, partials, "
"then fresh prompts, using older weight versions first within each stage"
)
self._heartbeats = threading.Thread(target=self._heartbeat_loop, name="queue-leases", daemon=True)
self._heartbeats.start()展开 Slime 源码:QueueReader.get_samples_async
QueueReader.get_samples_asyncslime/data/queue_data_source.py · L1113–L1115
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码
async def get_samples_async(self, num_samples):
"""Run blocking queue/file reads on the reader thread, keeping asyncio responsive."""
return await asyncio.get_running_loop().run_in_executor(self._reader, self.get_samples, num_samples)展开 Slime 源码:run_rollout_io
run_rollout_ioslime/data/transport.py · L370–L389
快照 2f2318653f6f794dddd321eff7c9d4b7b174643f· 查看固定版本源码
async def run_rollout_io(args, function, *values, **kwargs):
"""Bound the executor backlog before submitting encoding/sync work."""
loop = asyncio.get_running_loop()
key = (os.getpid(), loop)
if key not in _async_limits:
_async_limits[key] = asyncio.Semaphore(getattr(args, "rollout_io_concurrency", 4))
async with _async_limits[key]:
pending = asyncio.create_task(asyncio.to_thread(function, *values, **kwargs))
try:
return await asyncio.shield(pending)
except asyncio.CancelledError:
# Repeated cancellation still cannot stop a filesystem commit.
# Keep capacity and input ownership until the accepted write finishes.
while not pending.done():
try:
await asyncio.shield(pending)
except asyncio.CancelledError:
pass
pending.result()
raise不要把取消理解成“杀掉线程”
取消等待者,不会强行停止已经开始的线程函数。Slime 的 run_rollout_io() 会继续等待已经接受的写入完成,再传播取消或写入异常。这是结果所有权和文件完整性问题,不能靠多加一个 await 解决。
线程适合哪些工作,还取决于函数是否等待 I/O、是否释放 GIL,以及运行的 Python 实现。不要只凭“用了线程”就断言纯 Python 计算会多核加速。
复述检查
事件循环线程负责安排和等待;reader 的工作线程执行同步函数。await 挂起的是等待者,执行位置由 to_thread 或 executor 交接决定。
语义核对:to_thread、concurrent.futures。
本节对应的全部源码位置
下面把同类用法在 Slime 中的每个扫描位置接回本节讲解;点击文件名可查看完整源码快照。重复出现的 API 共用解释,具体输入与清理由所在函数决定。
展开本节 15 条源码记录
| 记录 / 讲解 | 源码位置 | 原代码 |
|---|---|---|
| occ-000592 | slime/backends/megatron_utils/server/megatron_server.py:356 _ray_get | return await asyncio.to_thread(ray.get, ref) |
| occ-000622 | slime/backends/megatron_utils/server/megatron_server.py:483 _build_http_app.update_from_disk | update_result = await asyncio.to_thread(update_from_disk_fn, model_path) |
| occ-000811 有接收者线索 | slime/data/queue_data_source.py:1115 QueueReader.get_samples_async | return await asyncio.get_running_loop().run_in_executor(self._reader, self.get_samples, num_samples) |
| occ-000840 | slime/data/transport.py:377 run_rollout_io | pending = asyncio.create_task(asyncio.to_thread(function, *values, **kwargs)) |
| occ-000851 | slime/data/transport.py:413 unpack_published_payload | return await asyncio.to_thread(unpack_rollout_payload, value) |
| occ-000853 | slime/data/transport.py:419 unpack_published_payload | return await asyncio.to_thread(unpack_rollout_payload, value) |
| occ-000987 | slime/rollout/fully_async_distributed.py:373 _GenerationActor._generate_group | await asyncio.to_thread( |
| occ-000991 | slime/rollout/fully_async_distributed.py:394 _GenerationActor._generate_group | group = await asyncio.to_thread(self.data_source.materialize_samples, group) |
| occ-001002 | slime/rollout/fully_async_distributed.py:413 _GenerationActor._flush_rebuffers | await asyncio.to_thread(self.data_source.add_samples, [group for group, _ in batch]) |
| occ-001065 | slime/rollout/fully_async_rollout.py:312 _generate_rollout_async | await asyncio.to_thread( |
| occ-001070 | slime/rollout/fully_async_rollout.py:357 _generate_rollout_async | output = await asyncio.to_thread( |
| occ-001163 | slime/rollout/sglang_rollout.py:545 generate_rollout_async | await asyncio.to_thread( |
| occ-001171 | slime/rollout/sglang_rollout.py:594 generate_rollout_async | output = await asyncio.to_thread( |
| occ-001257 | slime/utils/http_utils.py:204 _post | output = await asyncio.to_thread(decode_score_centering_response, content, score_centering_top_k) |
| occ-002102 | tests/test_straw_r3.py:163 test_cancelled_straw_publication_drains_before_releasing_capacity.run | assert await asyncio.to_thread(entered.wait, 5) |
专题首页 · 上一节 · 下一节:09 · Lock、Event 与线程退出 · 源码与教程对照 · 全部用法