不能直接用 queue.Queue 做异步爬虫任务队列,因其线程安全但同步阻塞,会阻塞事件循环;应使用原生支持 await 的 asyncio.Queue,并配合 task_done()、join() 和哨兵值实现安全协作式退出。

为什么不能直接用 queue.Queue 做爬虫任务队列?
因为 queue.Queue 是线程安全的,但默认不支持协程;在异步爬虫(如用 aiohttp)里直接塞进 queue.Queue 会阻塞整个事件循环。常见现象是:生产者能往队列塞 URL,但消费者卡住不动,CPU 占用低,日志没报错——本质是 get() 和 put() 都是同步阻塞调用,和 async/await 不兼容。
真正该用的是 asyncio.Queue,它原生支持 await,且内部用 asyncio.Event 实现唤醒机制。如果你硬要用 queue.Queue,只能搭配 threading + loop.run_in_executor,但这样绕路、难调试、容易漏掉异常。
如何用 asyncio.Queue 搭建最小可行爬虫队列?
核心是让生产者和消费者共用同一个 asyncio.Queue 实例,并通过 q.get() / q.put() 通信。注意:必须显式调用 q.task_done(),否则 q.join() 永远不会返回。
-
maxsize设为 0 表示无限制(默认),设为正整数可控制并发请求数上限,避免瞬间打爆目标站点 - 生产者用
await q.put(url),不要用q.put_nowait(),后者在满时抛asyncio.QueueFull - 消费者循环里用
try/except包住await q.get(),因为队列关闭后get()不会自动退出,需配合asyncio.wait_for或信号机制
import asyncio
q = asyncio.Queue(maxsize=10)
<p>async def producer():
for url in ["<a href="https://www.php.cn/link/5f69e19efaba426d62faeab93c308f5c">https://www.php.cn/link/5f69e19efaba426d62faeab93c308f5c</a>"] * 5:
await q.put(url)</p><p>async def consumer():
while True:
try:
url = await asyncio.wait_for(q.get(), timeout=1)</p><h1>这里发请求...</h1><pre class='brush:python;toolbar:false;'> print(f"fetching {url}")
q.task_done()
except asyncio.TimeoutError:
if q.empty():
break
q.join() 什么时候该等?怎么避免死锁?
q.join() 会阻塞直到所有已入队任务都被 task_done() 标记完成。如果消费者崩溃或漏调 task_done(),它就永远卡住。实际中建议只在主协程退出前等一次,且加超时保护。
安全 Python 代码沙箱:在隔离的沙箱中执行任意 Python 代码,预装 requests、NumPy、pandas 库。返回 stdout、stderr。
立即学习“Python免费学习笔记(深入)”;
- 不要在每个消费者里调
q.join()—— 它是队列级别的,不是每个消费者自己的 - 推荐模式:
asyncio.create_task(producer())启动生产者,再用asyncio.gather(*[consumer() for _ in range(3)])启多个消费者,最后await q.join() - 如果生产者是“有限生成”(比如读完一个列表就结束),可在生产完成后
await q.join();如果是长运行(监听文件变动),就别等,靠信号或定时退出
如何安全终止正在运行的生产者-消费者协程?
没有全局 kill 开关,得靠协作式退出:给队列塞特殊哨兵值(如 None),消费者收到后自行退出;或者用 asyncio.Event 控制生命周期。用 sys.exit() 或 os._exit() 会跳过协程清理,导致连接未关闭、文件未 flush。
- 哨兵法更轻量:生产者末尾
await q.put(None),消费者if url is None: break - 如果消费者数量多,要确保每个都收到哨兵 —— 所以得塞 N 个
None(N = 消费者数) - 用
asyncio.CancelledError捕获取消信号时,记得在finally块里调q.task_done(),否则join()卡死
真实爬虫里,URL 去重、失败重试、限速逻辑都得在消费者里做,队列只管传递原始 URL。把业务逻辑塞进队列本身,只会让调试变困难。

















