import asyncio
import aiohttp
CONCURRENCY = 5
URL = 'https://www.baidu.com'
semaphore = asyncio.Semaphore(CONCURRENCY)
session = None
async def scrape_api():
async with semaphore:
print('scraping',URL)
async with session.get(URL) as response:
await asyncio.sleep(1)
return await response.text()
async def main():
global session
session = aiohttp.ClientSession()
scrape_index_tasks = [asyncio.ensure_future(scrape_api()) for _ in range(10000)]
await asyncio.gather(*scrape_index_tasks)
if __name__=='__main__':
asyncio.get_event_loop().run_until_complete(main(
关键知识点:
asyncio.Semaphore(CONCURRENCY) 详细详解
asyncio.Semaphore(CONCURRENCY),它是 Python asyncio 库中用于异步场景限流的核心同步原语,专门解决异步协程并发数过高的问题,下面从定义、核心原理、使用方式、工作流程、注意事项等方面进行全面拆解。
一、核心定义与作用
1. 基本定义
asyncio.Semaphore(CONCURRENCY) 用于创建一个异步信号量对象 ,其中 CONCURRENCY 是传入的最大许可数 (即允许同时执行的协程最大数量),它是 asyncio 内置的同步原语,仅适用于异步协程环境。
2. 核心作用
在异步编程中,当有大量协程需要执行(比如你之前代码中的 10000 个 HTTP 请求协程),如果无限制并发,会导致目标服务器压力过大、本地端口耗尽、资源占用过高等问题。asyncio.Semaphore 的核心作用就是:限制异步协程的最大并发数,实现 "限流",保证系统资源被合理利用,同时避免过高并发带来的各类问题。
简单来说,它就像一个 "资源闸门",最多只允许 CONCURRENCY 个协程同时通过闸门执行后续任务,其他协程需要排队等待,直到有协程从闸门内退出(释放许可)。
二、核心原理:基于 "许可数" 的限流机制
asyncio.Semaphore 的底层工作逻辑围绕 **"许可(permit)"** 展开,核心原理可概括为 3 点:
- 初始化许可数 :创建
Semaphore对象时,传入的CONCURRENCY就是初始可用许可数(比如CONCURRENCY=5,就有 5 个初始许可); - 获取许可 :协程执行到
async with semaphore(或手动调用await semaphore.acquire())时,会尝试获取 1 个许可:- 若当前还有可用许可,直接获取成功,协程继续执行后续代码;
- 若当前无可用许可(已被其他协程耗尽),该协程会被挂起(暂停执行),并加入等待队列,不会阻塞整个事件循环,其他已获取许可的协程仍可正常执行;
- 释放许可 :当协程退出
async with semaphore代码块(或手动调用semaphore.release())时,会自动释放 1 个许可,将其归还到信号量的可用许可池中:- 释放后,信号量会从等待队列中唤醒一个挂起的协程,让它获取该许可并继续执行;
- 这个 "获取 - 释放" 的循环,保证了同时执行的协程数始终不超过
CONCURRENCY。
三、两种核心使用方式
asyncio.Semaphore 有两种常用使用方式,其中 ** 异步上下文管理器(async with)** 是推荐方式,更简洁安全。
方式 1:推荐 - 异步上下文管理器(async with)
这是 asyncio 推荐的最佳实践,它会自动完成 "获取许可" 和 "释放许可",无需手动处理,即使代码块中抛出异常,也能保证许可正常释放,避免资源泄露。
完整示例
import asyncio
# 定义最大并发数(许可数)
CONCURRENCY = 5
# 创建异步信号量对象
semaphore = asyncio.Semaphore(CONCURRENCY)
# 异步任务函数
async def async_task(task_id):
# 自动获取许可:进入代码块时获取,退出时释放
async with semaphore:
print(f"任务 {task_id}:获取许可,开始执行")
await asyncio.sleep(1) # 模拟异步耗时操作
print(f"任务 {task_id}:执行完成,释放许可")
# 主协程
async def main():
# 创建 20 个异步任务(远超最大并发数 5)
tasks = [asyncio.create_task(async_task(i)) for i in range(20)]
# 等待所有任务完成
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
运行结果如下:

执行特点
- 控制台会分批输出任务执行信息,每批最多 5 个任务(对应
CONCURRENCY=5); - 每批任务执行耗时约 1 秒,总耗时约 4 秒(20/5*1),体现了 "限流并发" 的高效性。
方式 2:手动调用 - acquire() 和 release()
这是更底层的使用方式,通过手动调用 await semaphore.acquire()(获取许可)和 semaphore.release()(释放许可)实现限流,灵活性更高,但需要手动保证两者成对出现,否则容易出现许可泄露。
完整示例
import asyncio
CONCURRENCY = 5
semaphore = asyncio.Semaphore(CONCURRENCY)
async def async_task(task_id):
try:
# 手动获取许可:必须使用 await,因为是异步操作
await semaphore.acquire()
print(f"任务 {task_id}:手动获取许可,开始执行")
await asyncio.sleep(1)
print(f"任务 {task_id}:执行完成,准备手动释放许可")
finally:
# 手动释放许可:放在 finally 中,保证即使抛出异常也能释放
semaphore.release()
async def main():
tasks = [asyncio.create_task(async_task(i)) for i in range(20)]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
注意事项
semaphore.acquire()是异步方法,必须配合await使用,否则无法获取许可,还会返回协程对象;semaphore.release()是同步方法,无需await,但必须保证在acquire()之后调用,且成对出现;- 推荐将
release()放在finally代码块中,避免代码块抛出异常导致许可无法释放,进而造成其他协程永久挂起。
四、关键工作流程(结合 CONCURRENCY=5)
结合代码之前的 10000 个 HTTP 请求协程为例,asyncio.Semaphore 的完整工作流程如下:
- 初始化阶段 :创建
semaphore = asyncio.Semaphore(5),可用许可数为 5,等待队列为空; - 第一批协程执行 :事件循环调度前 5 个协程,它们依次执行
async with semaphore,成功获取许可(可用许可数变为 0),进入后续 HTTP 请求逻辑; - 后续协程挂起 :第 6 个及以后的协程执行到
async with semaphore时,发现无可用许可,被依次挂起并加入等待队列,控制权交还事件循环; - 协程执行完成与许可释放 :1 秒后(
asyncio.sleep(1)结束),第一批 5 个协程依次执行完成,退出async with代码块,自动释放许可(可用许可数逐步恢复到 5); - 下一批协程唤醒:每次释放 1 个许可,信号量就从等待队列中唤醒 1 个挂起的协程,该协程获取许可后开始执行 HTTP 请求,可用许可数再次减少 1;
- 循环往复:重复步骤 3-5,直到所有 10000 个协程都执行完成,等待队列清空。
五、重要注意事项与补充
- 仅适用于异步协程环境 :
asyncio.Semaphore是asyncio库的组件,仅能在async def定义的协程函数中使用,不能在同步函数(def定义)中使用,也不能与threading库的线程混用(线程限流需使用threading.Semaphore); - 许可数的灵活性 :
CONCURRENCY通常设置为正整数,若不传入参数,默认初始许可数为 1(此时Semaphore等价于一个异步锁AsyncLock,保证同一时间只有 1 个协程执行); - 不会阻塞事件循环 :被
Semaphore挂起的协程,只是暂停自身执行,不会阻塞整个异步事件循环,其他已获取许可的协程或就绪的可等待对象仍可被事件循环调度执行,这是异步限流的核心优势; - 与
aiohttp配合的最佳实践 :在异步 HTTP 请求中,asyncio.Semaphore通常与aiohttp.ClientSession配合使用,既保证连接池复用,又限制并发请求数,避免触发目标服务器的反爬机制或端口耗尽; - 异常不影响许可释放 :无论是使用
async with还是try/finally包裹acquire()/release(),即使协程执行过程中抛出异常,许可也能正常释放,不会影响其他协程的执行。
总结
asyncio.Semaphore(CONCURRENCY)创建异步信号量对象,核心作用是限制异步协程的最大并发数,实现限流;- 底层基于 "许可数" 工作,通过 "获取 - 挂起 - 释放 - 唤醒" 的循环,保证并发数不超过
CONCURRENCY; - 推荐使用
async with异步上下文管理器,自动管理许可的获取与释放,安全简洁; - 仅适用于异步协程环境,不会阻塞事件循环,是异步编程中控制并发的核心工具。