Semaphore 用于硬性限制全局物理套接字创建上限,需进程级单例、跨线程协程共享、acquire/release 成对且异常安全,所有创建/销毁路径统一收口并及时释放许可,辅以监控与兜底机制。

在自研异步网络通信组件中,用 Semaphore 控制全局物理套接字句柄(SOCKET)创建上限,核心思路是:**所有创建套接字的操作必须先“申请许可”,成功后才调用 socket() 或 WSASocket();销毁时必须“归还许可”**。这不是装饰性限流,而是对操作系统资源的硬性守门。
关键设计点:全局唯一、跨线程/协程共享的信号量
物理套接字是系统级稀缺资源(Windows 默认上限约 16K,Linux 受 ulimit -n 约束),其分配发生在内核态。因此信号量必须:
- 在进程启动时一次性初始化为单例(如 Python 中用模块级变量 +
asyncio.Semaphore(N),C++ 中用std::shared_ptr配合静态存储期) - 不绑定到某个事件循环或线程局部存储——因为套接字可能在 I/O 线程创建,却在工作线程关闭,或由不同协程发起连接
- 确保 acquire/release 成对且异常安全(例如用
async with或 RAII 构造/析构)
套接字创建路径必须统一收口
不能允许任何绕过信号量的 socket() 调用。所有创建入口需封装,例如:
- 连接客户端:
connect_async(host, port)内部先await sem.acquire(),再调用WSASocket()+ConnectEx() - 监听服务器:
listen_on(port)同样需先获取许可,再socket()+bind()+listen() - 接受新连接:
accept_one()在AcceptEx()回调中完成套接字句柄提取后,立即计入全局计数(即每次AcceptEx成功返回一个新句柄,就相当于一次“创建”)
注意:AcceptEx 返回的是已建立连接的套接字,它和主动 connect 创建的句柄一样消耗系统资源,必须纳入同一信号量池。
销毁时务必释放许可,且时机要准
释放许可不能等到对象析构,而应在明确关闭套接字后立刻执行:
- 显式关闭:
closesocket()或shutdown() + closesocket()成功后,立即sem.release() - 避免放在析构函数里——若对象被遗忘或循环引用,许可将永久泄漏,上限很快耗尽
- 对 IOCP 或 WSAEventSelect 模型,需在
WSACloseEvent()和closesocket()都完成后再 release - 建议封装
SafeSocket类,在其close()方法末尾调用release(),并禁止裸用原始句柄
监控与兜底机制不可少
仅靠信号量无法防止资源泄露,需配套手段:
- 维护一个弱引用集合,记录当前所有活跃套接字句柄,定期扫描是否仍有未关闭句柄
- 在
acquire()超时失败时,记录告警并 dump 当前持有许可的协程/线程堆栈(Python 可用asyncio.all_tasks(),C++ 可结合 fiber ID 或日志上下文) - 设置合理初始值:通常取
min(系统限制 × 0.8, 预估峰值并发连接数 × 1.2),留出余量给监听套接字、UDP 句柄等其他用途










