
本文详解 asyncio 程序中因 aiohttp.ClientResponse 在上下文退出后自动关闭,导致 response.text() 调用阻塞或崩溃的根本原因,并提供安全读取响应体、合理复用会话与避免资源泄漏的完整解决方案。
本文详解 asyncio 程序中因 `aiohttp.clientresponse` 在上下文退出后自动关闭,导致 `response.text()` 调用阻塞或崩溃的根本原因,并提供安全读取响应体、合理复用会话与避免资源泄漏的完整解决方案。
在使用 aiohttp 进行异步 HTTP 请求时,一个常见却极易被忽视的陷阱是:ClientResponse 对象仅在其 async with 上下文中有效。一旦退出该上下文(即 async with session.get(...) as response: 的缩进块结束),响应连接会被自动关闭,底层字节流被释放——此时再尝试调用 await response.text() 或 await response.read() 将引发 RuntimeError: Connection closed,并导致协程永久挂起(表现即为“程序冻结”)。
你的代码中问题正源于此:
async def make_request(url, session):
async with session.get(url) as response: # ← 响应在此处创建
if response.ok:
return response # ← 返回的是已脱离上下文的 response 对象!
# ...随后在 get_image_url 中执行:
response = await make_request(url, session) # response 已无效 html = await response.text() # ❌ 此处冻结:Connection closed
✅ 正确做法:按需读取,不跨上下文传递响应对象
不应返回 response 对象本身,而应在 async with 块内完成所需数据的提取(如 .text()、.read() 或 .json()),并直接返回结果。为此,可重构 make_request,增加 return_text 参数控制行为:
import asyncio
from aiohttp import ClientSession
from concurrent.futures import ProcessPoolExecutor
from bs4 import BeautifulSoup
async def make_request(url, session, return_text=False):
async with session.get(url) as response:
if response.ok:
if return_text:
return await response.text() # ✅ 在上下文内读取并返回字符串
return response # 仅当后续仍需 status/headers 等元信息时才返回
else:
print(f'{url} returned: {response.status}')
return None if return_text else response对应地,更新 get_image_url 协程,直接获取 HTML 字符串:
async def get_image_url(pages_queue, image_urls_queue, session):
while True:
url = await pages_queue.get()
html = await make_request(url, session, return_text=True)
if not html:
pages_queue.task_done()
continue
# 解析交由进程池处理,避免阻塞事件循环
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
try:
image_url = await loop.run_in_executor(
pool, parse_link, html
)
await image_urls_queue.put(image_url)
except Exception as e:
print(f"Failed to parse {url}: {e}")
pages_queue.task_done()⚠️ 关键注意事项:
- 永远不要在
async with外部持有或使用ClientResponse实例;- 若需多次访问响应内容(如同时取
text()和headers),应在同一上下文中一次性读取并缓存;ProcessPoolExecutor的使用是合理的(BeautifulSoup是 CPU 密集型),但务必确保parse_link是纯函数且不依赖全局状态;get_image_page中未处理异常(如网络超时、DNS 失败),建议添加try/except并queue.task_done()保证队列正常退出;- 最终
main()中pages_queue.join()后手动cancel()任务虽可行,但更健壮的方式是使用asyncio.wait_for()或asyncio.shield()配合信号量控制生命周期。
通过将 I/O 提前至响应有效期内完成,你不仅解决了冻结问题,也使代码更符合 asyncio 的资源管理契约——清晰、安全、可维护。

















