asyncio.run()不可在已有事件循环的线程中重复调用;多线程协程混合应使用run_in_executor或to_thread;threading.local()不适用于协程上下文,需改用contextvars;日志需避免await表达式和阻塞io。

asyncio.run() 里不能直接启动 threading.Thread
主线程调用 asyncio.run() 后,事件循环在主线程中运行且被独占;此时若另起 threading.Thread 并在其中调用 asyncio.run(),会报 RuntimeError: asyncio.run() cannot be called from a running event loop —— 因为子线程虽独立,但如果你误在主线程的 loop 还没结束时又触发一次 asyncio.run(),就会撞上这个限制。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 需要多线程 + 多协程混合时,主线程用
asyncio.run()启动主协程,再用loop.run_in_executor()把阻塞操作(如文件读写、旧版同步 SDK 调用)扔进线程池,而不是手动建Thread - 如果真要跨线程运行协程(比如从后台线程触发异步通知),得用
asyncio.run_coroutine_threadsafe(coro, loop),并确保传入的是主线程的 loop 实例(通常需提前保存) - 避免在子线程里调用
asyncio.get_event_loop():它在非主线程默认返回新 loop,但该 loop 未运行,直接run_until_complete()会卡住或报错
threading.local() 在 async context 下不自动隔离
Python 的 threading.local() 靠线程 ID 做数据隔离,而 asyncio 协程常在同一线程内切换执行——所以你在协程 A 里给 local.x = 1,协程 B 可能读到它,除非你显式绑定到任务生命周期。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 不要依赖
threading.local()存储 request-id、db connection 等上下文敏感数据,协程调度会让它“泄漏” - 改用
contextvars.ContextVar:它感知协程切换,var.set()和var.get()自动绑定当前 task - 若必须兼容老代码,可在每个协程入口手动
local.x = copy.deepcopy(local.x),但性能差且易漏,不推荐
asyncio.to_thread() 是 Python 3.9+ 的安全替代方案
以前常用 loop.run_in_executor(None, sync_func, *args) 做 CPU/IO 密集型操作的异步封装,但它要手动管理 loop 引用,且在 asyncio.run() 结束后 loop 已关闭,再调用会出 RuntimeError: Event loop is closed。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- Python 3.9+ 直接用
await asyncio.to_thread(sync_func, *args):它自动选可用线程池,且不依赖用户传 loop,更健壮 - 注意
to_thread()仅适用于 IO 密集型或短时 CPU 操作;长耗时 CPU 计算仍应走ProcessPoolExecutor,否则会堵住整个 event loop - 3.9 以下版本可 pip 安装
anyio或手写兼容 wrapper,但别硬套run_in_executor到任意位置——尤其别在__aexit__或 signal handler 里调用,loop 可能已 teardown
logging.getLogger() 在多线程 + async 混合场景下容易丢日志
标准 logging 模块本身线程安全,但如果你在协程里用 logging.info() 打印了带 await 表达式的字符串(比如 f"result={await api_call()}"),那实际是先 await 再拼接再 log,中间可能被切走;更隐蔽的是,自定义 Handler 若含阻塞 IO(如写文件、发 HTTP),会拖慢整个 loop。
实操建议:
立即学习“Python免费学习笔记(深入)”;
- 所有日志消息内容必须是纯计算结果,禁止在
logging.xxx()参数里写await或耗时表达式 - 用
logging.handlers.QueueHandler+ 后台线程消费,把 IO 移出 event loop - 想打协程上下文(如 task name、trace id),用
contextvars+ 自定义Logger.filter()注入,别靠threading.local()拼凑
协程和线程的边界比看起来薄得多,一个 await 调用背后可能藏着 loop 切换、线程池调度、context 变更——别假设“只要没用 time.sleep() 就还是 async”。










