深入 async-sema 源码:基于令牌的信号量机制是如何运作的?
深入 async-sema 源码基于令牌的信号量机制是如何运作的【免费下载链接】async-semaSemaphore using async and await项目地址: https://gitcode.com/gh_mirrors/as/async-sema当 100 个并发请求同时涌向你的 Node.js 服务如何保证它们有序、有度地执行答案就是信号量机制Semaphore。async-sema 正是这样一个基于async/await的轻量信号量库它没有使用传统的计数器而是用一套独特的令牌池来管理并发。今天我们就深入源码把这套基于令牌的信号量机制拆开看个明白。为什么并发代码需要信号量机制先看一个生活场景一家只有 4 个座位的咖啡馆同时来了 30 位顾客。如果全部涌入店里会乱作一团。信号量机制就是那扇限流门——只放 4 位顾客进店其余人在门外排队有人离开才放进下一位。写代码时同理数据库连接池只有 5 个连接API 供应商只允许每秒 3 次请求你绝不能放任代码无限并发。信号量机制能精确地限制同时执行的任务数量这正是 async-sema 存在的意义。信号量机制的核心三要素可用令牌当前还能放行几个任务等待队列抢不到令牌的任务在这里排队唤醒逻辑令牌被释放后如何叫醒队首的等待者async-sema 是什么一个基于 async/await 的轻量信号量库async-sema 是一个遵循传统信号量定义的 JavaScript 库——只允许指定数量的任务同时执行其余任务保持等待。它与 JS 社区里常见的异步信号量有本质区别对比维度传统异步信号量async-sema 信号量机制任务执行所有任务立即执行只有拿到令牌的任务执行同步时机最后统一同步全程按令牌放行并发控制弱强适用场景简单的等全部完成限流、连接池、资源池整个库只有一个 src/index.ts 文件Sema 类约 100 行零依赖却撑起了 Vercel 等大型项目的并发控制需求轻巧得令人惊叹。核心设计为什么用令牌池替代数字计数器大多数信号量实现用一个数字记录剩余资源比如count 3。但 async-sema 的开发者做了一个更妙的设计用一组真实存在的令牌对象来代表可用资源。在 Sema 构造函数 中initFn被反复调用为每一个可用名额生成一个令牌const s new Sema(4, { initFn: () token }); // 内部会生成 4 个令牌放入空闲队列亮点来了令牌可以是任意值如果你把令牌替换成真实的数据库连接信号量机制就摇身一变成了资源池管理器。官方示例 examples/pooling.js 就是这么干的——initFn直接创建 Redis 客户端acquire 拿到的就是一个现成的连接用完 release 归还优雅至极。三大数据结构信号量机制的心脏信号量机制的全部奥秘都藏在这三个成员变量里见 src/index.tsfree空闲令牌队列存放当前可用的令牌acquire 从这里取waiting等待队列存放拿不到令牌而挂起的 Promiserelease 从这里唤醒releaseEmitter释放事件器release 时广播消息驱动整个唤醒流程两个队列都不是普通数组而是一个环形缓冲区 Dequesrc/index.ts。它把容量对齐到 2 的幂次用 (capacity - 1)位运算代替取模push 和 shift 都是 O(1) 时间复杂度——在高并发下性能优势非常明显。acquire 获取令牌一次优雅的排队acquire是信号量机制的入口流程只有四步src/index.ts先从free队列弹出令牌弹到了直接返回任务立即执行 ✅没弹到创建一个 Promise推入waiting队列返回这个 Promise任务挂起等待任务调用 acquire() │ ▼ free 队列有令牌 ──是──▶ 立即返回令牌任务执行 │否 ▼ 推入 waiting 队列任务挂起 │ ▼ release 时被唤醒拿到令牌继续执行如果不想等待还有非阻塞的tryAcquire()src/index.ts有令牌立刻返回没有就返回undefined绝不排队。这在需要抢不到就跳过的场景非常实用。release 释放令牌如何唤醒等待者release的设计是信号量机制最精妙的一环——它不直接操作队列而是通过事件驱动src/index.tsrelease(token) │ ▼ 触发 release 事件 │ ├── waiting 队列有人等待──▶ 直接把令牌交给队首等待者resolve 它 │ └── 没人等待──▶ 令牌放回 free 队列留给后来的任务关键点在于令牌被释放时如果有人在排队令牌会直接跳队交给等待者而不会先放回空闲队列再被取走。这样既避免了不必要的队列往返也天然保证了公平性——先等待的先拿到令牌。高级技巧pauseFn 和 resumeFn 防止内存溢出当任务源源不断涌入且全都拿不到令牌时waiting队列会无限膨胀最终撑爆内存。async-sema 信号量机制对此有内置解法pauseFn/resumeFn背压机制。队列开始堆积时自动调用pauseFn()通知外部暂停投喂任务队列清空后自动调用resumeFn()通知外部可以继续了官方示例 examples/pausing.js 用它控制readline流任务积压时暂停读取 stdin处理完再恢复——形成一个完美的背压闭环大文件处理也不怕内存溢出。对应的测试见 test/sema.test.ts。彩蛋基于信号量的 RateLimit 限流器async-sema 还顺手实现了一个极简限流器RateLimitsrc/index.ts原理只有三行创建信号量令牌数 每秒允许的请求数rps每次调用先acquire()拿令牌用setTimeout在时间窗口后release()归还令牌const lim RateLimit(5); // 每秒最多 5 次 await lim(); // 超限时这里会阻塞等待 // ... 执行你的异步操作开启uniformDistribution: true后它会改用new Sema(1)让请求在时间窗口内均匀分布避免前 5 个请求瞬间放行、后面全部卡死的突发模式非常适合平滑调用第三方 API。完整用法见 examples/rate-limiting.js。5 分钟上手一个完整的信号量使用示例安装只需一条命令npm install async-sema最经典的用法是控制并发数参考 examples/basic.jsconst { Sema } require(async-sema); // 只允许 4 个任务同时执行 const s new Sema(4); async function fetchData(x) { await s.acquire(); // 抢令牌抢不到就排队 try { // ... 你的异步任务 } finally { s.release(); // 务必释放否则令牌会耗尽 } } await Promise.all(array.map(fetchData));想亲手调试这套信号量机制的源码clone 下来即可git clone https://gitcode.com/gh_mirrors/as/async-sema小结async-sema 用不到 150 行代码就把信号量机制做到了优雅与性能的平衡令牌池替代计数器、双队列加事件驱动、环形缓冲区极致提速、背压机制防内存溢出。理解这套基于令牌的信号量机制后你会发现并发控制不再是玄学——限流、连接池、资源管理一个信号量全都搞定。如果你正在为 Node.js 应用的并发失控而头疼不妨从读懂 async-sema 的源码开始。【免费下载链接】async-semaSemaphore using async and await项目地址: https://gitcode.com/gh_mirrors/as/async-sema创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻