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