百万网页下载:固定协程工作者VS单URL协程+asyncio.Semaphore?
百万网页下载:固定协程工作者 vs Semaphore限制协程
针对百万级URL的下载需求,两种方案的差异和选择建议如下:
方案一:固定协程工作者 + 任务队列
核心逻辑
创建固定数量的worker协程(示例中为10个),先将所有URL放入异步队列,worker持续从队列中取出URL执行下载,直到队列空了才退出。
async def worker(queue): while True: try: url = queue.get_nowait() except QueueEmpty: break await download(url) async def main(): queue = asyncio.Queue() for url in urls: await queue.put(url) workers = [worker(queue) for _ in range(10)] await asyncio.wait(workers) asyncio.run(main())
优势
- 内存占用极低:仅固定数量的协程处于活跃状态,百万URL仅以字符串形式存储在队列中,内存消耗完全可控,不会因协程对象过多导致内存溢出。
- 调度开销小:事件循环只需管理少量活跃协程,调度压力低,整体运行更稳定。
- 扩展性强:即使任务量翻倍,只需调整worker数量即可适配,无需改动核心逻辑。
不足
需要手动维护任务队列,代码多了队列初始化、入队的步骤,逻辑稍显繁琐。
方案二:Semaphore + 每个URL一个协程
核心逻辑
为每个URL单独创建一个协程,用asyncio.Semaphore限制同时执行的协程数量(示例中为10个),超出数量的协程会等待信号量释放后再执行。
async def worker(url, sema): async with sema: await download(url) async def main(): semaphore = asyncio.Semaphore(10) workers = [worker(url, semaphore) for url in urls] await asyncio.wait(workers) asyncio.run(main())
优势
- 代码简洁直观:无需维护队列,直接为每个任务生成协程,用信号量控制并发,逻辑一目了然。
- 问题定位方便:每个协程对应一个URL,单个任务出错时更容易追踪具体问题。
不足
- 内存压力大:百万个协程对象(即使处于等待状态)会占用大量内存,可能导致内存资源紧张。
- 调度成本高:事件循环需要管理百万个等待中的协程,调度开销会显著增加,影响整体执行效率。
最终选择建议
如果是百万级这种超大规模的下载任务,优先选择方案一——固定协程工作者+队列的模式,它能更好地控制内存和调度开销,保证任务稳定运行。
如果任务量较小(比如几万级URL),方案二的简洁性更有优势,开发成本更低。
内容的提问来源于stack exchange,提问作者ospider
相关产品推荐
相关产品推荐

