# -*- coding: utf-8 -*- """同步 ↔ 异步桥(迁移期用)。 场景:部分调用方仍是同步代码(遗留 Flask 路由、线程池下载器),而服务层已异步。 `run_sync(coro)` 把协程提交到**专用后台事件循环线程**执行并阻塞等待结果, 使同步代码得以调用异步实现,无需把调用链一次性全部改异步。 - 后台 loop 常驻单例(daemon 线程),避免每个请求新建 loop 的开销与 aiohttp 连接池失效; - 在已有事件循环中调用 `run_sync` 会死锁,故检测到运行中的 loop 时直接报错, 提示改用 `await`。 """ import asyncio import threading from collections.abc import Coroutine from typing import Any _loop: asyncio.AbstractEventLoop | None = None _loop_lock = threading.Lock() def _get_loop() -> asyncio.AbstractEventLoop: global _loop if _loop is not None and not _loop.is_closed(): return _loop with _loop_lock: if _loop is None or _loop.is_closed(): loop = asyncio.new_event_loop() t = threading.Thread(target=loop.run_forever, name='async-bridge', daemon=True) t.start() _loop = loop return _loop def get_loop() -> asyncio.AbstractEventLoop: """后台事件循环(供需要直接提交协程的调用方使用)。""" return _get_loop() def run_sync[T](coro: Coroutine[Any, Any, T], timeout: float | None = None) -> T: """在同步代码中运行协程并等待结果。 Raises: RuntimeError: 在已有事件循环内调用(应直接 await 而非桥接)。 """ try: asyncio.get_running_loop() except RuntimeError: pass else: raise RuntimeError('run_sync 不能在事件循环内调用,请直接 await 协程') fut = asyncio.run_coroutine_threadsafe(coro, _get_loop()) return fut.result(timeout) def shutdown() -> None: """停止后台 loop(应用退出时调用)。""" global _loop if _loop is not None and not _loop.is_closed(): _loop.call_soon_threadsafe(_loop.stop) _loop = None