求助:异步代码中调用同步库时复用现有异步回调的方案
解决方案:在WeasyPrint同步回调中复用异步aiohttp会话
核心问题分析
你需要在WeasyPrint的同步URL获取回调中,复用已配置好中间件的aiohttp异步客户端会话,避免重复编写同步HTTP请求代码,但此前两种方案因asyncio线程模型限制失败:
- 方案一:在子线程新建事件循环调用异步代码,但aiohttp ClientSession与创建它的事件循环绑定,跨循环使用直接报错。
- 方案二:用
asyncio.to_thread运行WeasyPrint,但在回调中直接调用主线程事件循环的run_until_complete,触发“事件循环已在运行”的错误。
可行方案:使用asyncio.run_coroutine_threadsafe
asyncio.run_coroutine_threadsafe是专门用于在非事件循环线程中,向运行中的事件循环提交协程的工具。它返回concurrent.futures.Future对象,可通过result()方法阻塞等待异步任务完成,完美适配WeasyPrint的同步回调需求。
完整实现代码
import asyncio import io from weasyprint import HTML import aiohttp async def print_weasyprint(html: io.IOBase, resources: aiohttp.ClientSession): # 获取主线程运行的事件循环 loop = asyncio.get_running_loop() # 生成WeasyPrint所需的同步URL回调 url_fetcher = _make_url_fetcher(resources, loop) # 将WeasyPrint同步逻辑放入子线程执行,避免阻塞主线程事件循环 return await asyncio.to_thread(_print_weasyprint_sync, html, url_fetcher) def _print_weasyprint_sync(html: io.IOBase, url_fetcher): out = io.BytesIO() HTML( file_obj=html, url_fetcher=url_fetcher, ).write_pdf(out) out.seek(0) return out def _make_url_fetcher(session: aiohttp.ClientSession, loop: asyncio.AbstractEventLoop): def fetcher(url, *args, **kwargs): # 提交异步请求协程到主线程事件循环 future = asyncio.run_coroutine_threadsafe(_fetch(session, url), loop) # 阻塞等待异步请求结果返回 content, content_type = future.result() return { 'string': content, 'mime_type': content_type } return fetcher async def _fetch(session: aiohttp.ClientSession, url: str): async with session.get(url) as resp: return await resp.content.read(), resp.content_type
关键细节说明
asyncio.run_coroutine_threadsafe的作用:将异步协程提交给指定的事件循环(主线程循环),返回可在子线程中阻塞等待的Future对象,解决跨线程调用异步代码的核心问题。- WeasyPrint线程隔离:用
asyncio.to_thread将WeasyPrint的同步逻辑放到子线程执行,避免其阻塞主线程的事件循环,符合asyncio最佳实践。 - 会话完全复用:
_fetch函数直接使用主线程创建的aiohttp ClientSession,完全复用已有的中间件、超时等配置,无需编写重复的同步HTTP请求代码。
注意事项
- 确保
run_coroutine_threadsafe传入的事件循环是创建ClientSession的那个循环,否则仍会出现会话与循环不匹配的错误。 - 可给
future.result()设置超时时间(如future.result(timeout=10)),避免因异步请求长时间无响应导致WeasyPrint卡住。
内容的提问来源于stack exchange,提问作者DMJ
相关产品推荐
相关产品推荐

