You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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;
    }
}

核心优化说明

  1. 移除忙等循环:用ThreadPool.RegisterWaitForSingleObject替代轮询逻辑,该方法将事件等待交给线程池管理,只有当IO完成或超时触发时才会执行回调,完全避免了CPU空转。
  2. 异步模型适配:通过TaskCompletionSource创建异步任务,无需额外开启长期线程,完美契合async-await的非阻塞编程模式。
  3. 取消逻辑处理:注册CancellationToken回调,触发取消时调用CancelIo终止异步IO,并正确设置任务的取消状态,保证取消逻辑的一致性。
  4. 资源安全:在操作完成、取消或异常时,确保Win32事件句柄被正确释放,避免系统资源泄漏。

额外注意事项

  • 打开串口句柄时,必须指定FILE_FLAG_OVERLAPPED标志,否则无法启用异步IO功能。
  • 若需要跨线程取消IO操作,建议使用CancelIoEx(Windows Vista及以上系统支持)替代CancelIo,因为CancelIo仅能取消当前线程发起的IO操作。
  • 可通过修改ThreadPool.RegisterWaitForSingleObject的超时参数,添加自定义超时逻辑。

内容的提问来源于stack exchange,提问作者Fanan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 09:14:52