异步函数里调用同步函数,会发生什么

如果这个同步函数正在读磁盘、等待同步网络请求或做耗时处理,事件循环的线程会被它占住。把外层函数写成 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_async
slime/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_io
slime/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-000592slime/backends/megatron_utils/server/megatron_server.py:356
_ray_get
return await asyncio.to_thread(ray.get, ref)
occ-000622slime/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-000840slime/data/transport.py:377
run_rollout_io
pending = asyncio.create_task(asyncio.to_thread(function, *values, **kwargs))
occ-000851slime/data/transport.py:413
unpack_published_payload
return await asyncio.to_thread(unpack_rollout_payload, value)
occ-000853slime/data/transport.py:419
unpack_published_payload
return await asyncio.to_thread(unpack_rollout_payload, value)
occ-000987slime/rollout/fully_async_distributed.py:373
_GenerationActor._generate_group
await asyncio.to_thread(
occ-000991slime/rollout/fully_async_distributed.py:394
_GenerationActor._generate_group
group = await asyncio.to_thread(self.data_source.materialize_samples, group)
occ-001002slime/rollout/fully_async_distributed.py:413
_GenerationActor._flush_rebuffers
await asyncio.to_thread(self.data_source.add_samples, [group for group, _ in batch])
occ-001065slime/rollout/fully_async_rollout.py:312
_generate_rollout_async
await asyncio.to_thread(
occ-001070slime/rollout/fully_async_rollout.py:357
_generate_rollout_async
output = await asyncio.to_thread(
occ-001163slime/rollout/sglang_rollout.py:545
generate_rollout_async
await asyncio.to_thread(
occ-001171slime/rollout/sglang_rollout.py:594
generate_rollout_async
output = await asyncio.to_thread(
occ-001257slime/utils/http_utils.py:204
_post
output = await asyncio.to_thread(decode_score_centering_response, content, score_centering_top_k)
occ-002102tests/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 与线程退出 · 源码与教程对照 · 全部用法