同步I/O在Windows中通过阻塞当前线程实现,需FILE_FLAG_OVERLAPPED才能启用IOCP异步机制;FileStream默认不启用IOCP(.NET 5+已改),小文件同步读反而增加开销;async/await有效性取决于底层是否真异步。
同步I/O调用(如 FileStream.Read() 或 File.ReadAllText())最终会通过Win32 API(如 ReadFile())进入内核。当文件句柄是同步打开的(即未指定 FILE_FLAG_OVERLAPPED),内核会直接在当前线程的内核栈上发起IRP(I/O Request Packet),并让该线程进入等待状态(WaitForSingleObject 类语义)。此时线程被挂起,不消耗CPU,但占用一个线程栈(默认1MB)和线程对象资源。
常见错误现象:在ASP.NET Core中大量使用 File.ReadAllBytes() 处理上传文件,导致线程池耗尽、请求排队、响应延迟陡增。
Socket.Receive())同样适用:未设 SO_RCVTIMEO 时会无限期等待对方发包C#中 FileStream.ReadAsync() 或 Socket.ReceiveAsync() 底层依赖Windows的I/O Completion Port(IOCP)。关键在于:文件/套接字必须以 FILE_FLAG_OVERLAPPED 标志打开(.NET内部自动处理),且I/O请求通过 ReadFileEx() 或 WSARecv() 提交,不等待完成,立即返回。
完成通知不靠轮询或新线程,而是由内核在I/O结束后将完成包投递到绑定的IOCP句柄;.NET线程池中的某个线程(非发起线程)调用 GetQueuedCompletionStatus() 取出结果
并调度回调(如 await 后续代码)。
async/await 本身不保证底层是异步I/O,比如 MemoryStream.ReadAsync() 实际是同步内存拷贝+Task.CompletedTask.NET的 FileStream 构造函数中,useAsync: true 参数决定是否启用内核级异步I/O。但默认值是 false(.NET 5+ 已改为 true,但旧项目或显式传 false 仍存在)。原因很实际:
ThreadPoolWorkQueue.Dispatch()
验证方式:用Process Explorer查看进程句柄,同步打开的文件句柄类型为 File,异步打开的会显示 File (Overlapped)。
最典型的反模式是「伪异步」:用 Task.Run(() => File.ReadAllBytes()) 包裹同步I/O。这没减少I/O等待,只是把阻塞从主线程移到了线程池线程,反而增加调度和上下文切换负担。
SqlConnection.Open() 是同步的,OpenAsync() 才触发真正的异步登录流程(基于 WSAConnectEx)async 方法里调用 Request.Body.Read()(同步)会导致整个请求管道阻塞[AsyncStateMachine] 或内部用 Thread.Sleep() 模拟延迟,await 不会释放线程真正关键的分水岭不在C#语法,而在那个内核句柄是不是 OVERLAPPED——其余都是包装和调度策略。