同步Socket在高并发下会迅速卡死
同步调用
Socket.Receive()或
Socket.Send()时,线程会一直阻塞直到数据收发完成。哪怕只是处理几百个长连接,用
Thread每连接起一个线程,很快就会耗尽线程池资源,出现大量
ThreadAbortException或响应延迟飙升。这不是代码写得不好,而是模型本身无法横向扩展。
异步Socket真正靠的是 SocketAsyncEventArgs
复用
很多人以为用
BeginReceive/
EndReceive就算“异步”了,其实那是基于线程池的伪异步,开销不小。真正高性能的做法是预分配一批
SocketAsyncEventArgs实例,反复
SetBuffer+
AcceptAsync/
ReceiveAsync,避免每次收发都 new 对象、触发 GC。关键点:
SocketAsyncEventArgs必须手动调用
Dispose()(通常在连接关闭时) 缓冲区最好用
ArrayPool<byte>.Shared.Rent()</byte>管理,而不是每次都
new byte[8192]
ReceiveAsync返回
false表示同步完成,
true才进回调 —— 别默认以为一定异步
吞吐量差距在真实场景中可达 5–10 倍
用相同硬件压测一个回显服务(单机 4 核 8G):
同步模式(每连接一线程):约 1200 QPS,CPU 95% 时连接数卡在 300 左右 异步模式(单线程 EventLoop + SocketAsyncEventArgs 池):稳定 8500+ QPS,CPU 利用率 65%~75%
差距主要来自三方面:
内存分配:同步模式每连接至少多出 1MB 线程栈 + 频繁 buffer new;异步模式 buffer 复用后 GC 压力极低 上下文切换:300 个线程调度开销远高于 1 个线程处理 3000 个 socket 的 I/O 完成通知 系统调用密度:异步模式下WSARecv调用更紧凑,更容易被 IOCP 批量投递
别忽略 IOCompletionPort
的隐式绑定成本
.NET 的
Socket异步方法底层走 Windows IOCP,但首次调用
ReceiveAsync时才会把 socket 关联到线程池的完成端口 —— 这个过程有微小延迟。如果连接建立后立刻发数据,可能因端口未就绪导致第一次收包稍慢。解决办法很简单: 在
AcceptAsync成功后,立即对新 socket 调用一次空 buffer 的
ReceiveAsync(不等回调返回),强制绑定 或改用
ThreadPool.UnsafeQueueUserWorkItem启动一个轻量初始化任务,提前触发绑定
这个细节在压测初期不容易暴露,但上线后偶发的首包延迟,往往就卡在这里。
