如何在C#中实现Windows异步串口读取并避免忙等循环?
如何避免异步串口读写中的忙等循环(基于WinAPI IOCP)
要彻底消除忙等循环,你可以结合TaskCompletionSource与Win32的事件通知机制,同时利用线程池的等待API处理异步IO完成事件,既不需要轮询,也能优雅支持取消令牌,且贴合.NET的async-await异步模型。
优化后的实现代码
using System; using System.Threading; using System.Threading.Tasks; using System.Runtime.InteropServices; using System.ComponentModel; public class CustomSerialPort { private IntPtr _handle = IntPtr.Zero; // WinAPI 函数声明 [DllImport("kernel32.dll", SetLastError = true)] private static extern bool ReadFile(IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, ref OVERLAPPED lpOverlapped); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetOverlappedResult(IntPtr hFile, ref OVERLAPPED lpOverlapped, out uint lpNumberOfBytesTransferred, bool bWait); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr CreateEvent(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CancelIo(IntPtr hFile); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject); [StructLayout(LayoutKind.Sequential)] private struct OVERLAPPED { public uint Internal; public uint InternalHigh; public uint Offset; public uint OffsetHigh; public IntPtr hEvent; } public Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { if (_handle == IntPtr.Zero) { throw new InvalidOperationException("串口未打开"); } // 参数合法性校验 if (buffer == null) throw new ArgumentNullException(nameof(buffer)); if (offset < 0 || offset >= buffer.Length) throw new ArgumentOutOfRangeException(nameof(offset)); if (count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException(nameof(count)); byte[] tempBuffer = new byte[count]; var overlapped = new OVERLAPPED { hEvent = CreateEvent(IntPtr.Zero, true, false, null) }; uint bytesRead = 0; bool ioPending = false; var tcs = new TaskCompletionSource<int>(); try { // 发起异步读操作 if (!ReadFile(_handle, tempBuffer, (uint)count, out bytesRead, ref overlapped)) { int error = Marshal.GetLastWin32Error(); if (error != 997) // ERROR_IO_PENDING:操作已异步挂起 { tcs.SetException(new IOException($"ReadFile调用失败,错误码: {error}", new Win32Exception(error))); return tcs.Task; } ioPending = true; } // 操作已同步完成,直接返回结果 if (!ioPending) { Array.Copy(tempBuffer, 0, buffer, offset, bytesRead); tcs.SetResult((int)bytesRead); return tcs.Task; } // 注册取消令牌回调:触发取消时终止IO操作 var cancellationRegistration = cancellationToken.Register(() => { if (CancelIo(_handle)) { tcs.TrySetCanceled(cancellationToken); } else { int error = Marshal.GetLastWin32Error(); tcs.TrySetException(new IOException($"取消IO操作失败,错误码: {error}", new Win32Exception(error))); } CloseHandle(overlapped.hEvent); }); // 委托线程池监听IO完成事件,避免忙等 ThreadPool.RegisterWaitForSingleObject( overlapped.hEvent, (state, timedOut) => { cancellationRegistration.Dispose(); try { if (timedOut) { tcs.SetException(new TimeoutException("IO操作超时")); return; } // 检查是否已取消 if (cancellationToken.IsCancellationRequested) { tcs.TrySetCanceled(cancellationToken); return; } // 获取异步IO操作结果 if (!GetOverlappedResult(_handle, ref overlapped, out bytesRead, false)) { int error = Marshal.GetLastWin32Error(); if (error == 995) // ERROR_OPERATION_ABORTED:操作被取消 { tcs.TrySetCanceled(cancellationToken); } else { tcs.SetException(new IOException($"获取IO结果失败,错误码: {error}", new Win32Exception(error))); } return; } // 复制结果到目标缓冲区 Array.Copy(tempBuffer, 0, buffer, offset, bytesRead); tcs.SetResult((int)bytesRead); } finally { // 释放事件句柄,避免资源泄漏 CloseHandle(overlapped.hEvent); } }, null, Timeout.Infinite, // 无限等待,可根据需求设置超时时间 executeOnlyOnce: true); } catch (Exception ex) { tcs.SetException(ex); CloseHandle(overlapped.hEvent); } return tcs.Task; } }
核心优化说明
- 移除忙等循环:用
ThreadPool.RegisterWaitForSingleObject替代轮询逻辑,该方法将事件等待交给线程池管理,只有当IO完成或超时触发时才会执行回调,完全避免了CPU空转。 - 异步模型适配:通过
TaskCompletionSource创建异步任务,无需额外开启长期线程,完美契合async-await的非阻塞编程模式。 - 取消逻辑处理:注册
CancellationToken回调,触发取消时调用CancelIo终止异步IO,并正确设置任务的取消状态,保证取消逻辑的一致性。 - 资源安全:在操作完成、取消或异常时,确保Win32事件句柄被正确释放,避免系统资源泄漏。
额外注意事项
- 打开串口句柄时,必须指定
FILE_FLAG_OVERLAPPED标志,否则无法启用异步IO功能。 - 若需要跨线程取消IO操作,建议使用
CancelIoEx(Windows Vista及以上系统支持)替代CancelIo,因为CancelIo仅能取消当前线程发起的IO操作。 - 可通过修改
ThreadPool.RegisterWaitForSingleObject的超时参数,添加自定义超时逻辑。
内容的提问来源于stack exchange,提问作者Fanan
相关产品推荐
相关产品推荐

