DynamicSemaphore类通过封装Semaphore并用RLock保护容量变更,实现运行时动态调节并发度;任务需携带上下文标识(如domain、task_id)以支持域名级限流与实时监控;调节逻辑嵌入请求生命周期,基于滑动窗口统计异步执行,且取消任务时严格保证semaphore计数一致。

asyncio.Semaphore 的并发度必须在运行时可调
硬编码 asyncio.Semaphore(10) 会导致爬虫上线后无法根据下游响应延迟、错误率或 CPU/网络负载动态缩放并发数。关键不是“怎么加锁”,而是“怎么让锁的容量可变”。直接替换 semaphore._value 是危险的(内部属性、无同步保障),正确做法是用一个封装类代理 acquire/release,并在内部原子更新计数器和信号量实例。
- 初始化时创建一个可变容量的
DynamicSemaphore类,内部持有当前asyncio.Semaphore实例和一个threading.RLock保护容量变更 - 调用
set_concurrency(n)时:若n > current,仅需增加内部计数;若n ,需等待正在运行的协程自然退出,不强制中断任务 - 每次
acquire()前检查是否允许进入——不是靠信号量阻塞,而是先走一层轻量级许可判断,避免高并发下调用semaphore.acquire()频繁触发事件循环调度开销
请求任务必须携带上下文标识用于实时监控与限流干预
没有上下文的 fetch(url) 调用无法区分是重试、慢速域名还是新种子,也就没法做 per-domain 并发控制或基于 RTT 的自适应降级。每个任务应包装成带元数据的 RequestTask 对象,至少包含 url、domain、retry_count、start_time 和唯一 task_id。
- 域名级并发限制不能只靠全局信号量:对
example.com单独维护一个asyncio.Semaphore,键为parse_domain(url),用weakref.WeakValueDictionary管理,避免内存泄漏 - 在
fetch()开头记录time.time(),结束时计算耗时;若单次超时(如 >8s)且错误率上升,自动对该 domain 的并发上限减半,持续 60 秒 - 把
task_id透传到日志和 metrics 上报中,方便 Prometheus 抓取时按 task 分组聚合成功率、P95 延迟等指标
动态调节不能依赖定时轮询,得靠反馈闭环驱动
每秒检查一次错误率再改并发数,本质是开环控制——等你发现失败率飙升,队列可能已积压数百个 pending task。真实有效的调节必须嵌入请求生命周期:在每次 session.get() 完成后立即评估,结合本次结果和最近 30 秒滑动窗口统计做决策。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
- 维护一个线程安全的滑动窗口结构(如
collections.deque存 (timestamp, success, duration) 元组),长度按时间裁剪(非固定 size),避免内存无限增长 - 当连续 3 次请求返回 5xx 或连接超时,立刻将全局并发度设为原值的 0.5 倍(最小为 1);恢复条件是后续 10 次成功且平均延迟下降 20%
- 调节动作本身要异步化:调用
adjust_concurrency(new_value)后不阻塞当前协程,而是提交到事件循环后台执行,防止调节逻辑拖慢主请求流
取消任务时必须保证 semaphore 计数与实际运行数严格一致
用户 Ctrl+C 或主动调用 cancel() 时,如果只取消 task 而不释放它已持有的 semaphore.acquire(),后续所有请求都会卡死。Python 的 asyncio.Task.cancel() 不会自动触发 __aexit__,必须显式处理。
立即学习“Python免费学习笔记(深入)”;
- 所有
acquire()必须放在async with semaphore:块内,且该块必须包裹完整请求逻辑(包括重试循环) - 在
except asyncio.CancelledError:分支中,确保调用semaphore.release()——但注意:如果 acquire 尚未完成,不能 release,所以要用try/finally包裹 acquire 步骤本身 - 更稳妥的做法是用
asyncio.shield()包住semaphore.acquire(),防止 acquire 过程被中途取消导致状态错乱

















