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

如何将DeviceIoControl异步操作正确转换为Task实现?

异步CD-ROM读取:OVERLAPPED转Task的正确性分析与改进

我使用DeviceIoControl结合SPTD读取CD-ROM,已将代码升级为异步版本,代码能正常运行,但不确定OVERLAPPED到Task的转换逻辑是否完全正确,以下是相关代码:

ReadSectorAsync 方法

public override unsafe Task<ISector> ReadSectorAsync(in int index)
{
    using var manualResetEvent = new ManualResetEvent(true);

    var overlapped = new Overlapped
    {
        EventHandleIntPtr = manualResetEvent.SafeWaitHandle.DangerousGetHandle(),
    };

    var nativeOverlapped = overlapped.Pack(null, null);

    var memory = Disc.GetDeviceAlignedBuffer(2352, Handle);

    var buffer = memory.Span;

    const int timeOutValue = 3;

    fixed (byte* data = &MemoryMarshal.GetReference(buffer))
    {
        var inBufferSize = (uint)Marshal.SizeOf<NativeTypes.SCSI_PASS_THROUGH_DIRECT>();
        var inBuffer = Marshal.AllocHGlobal((int)inBufferSize);

        var position = index;

        // ReSharper disable once ConvertToConstant.Local
        var transfer = 1u; // sectors

        var sptd = new NativeTypes.SCSI_PASS_THROUGH_DIRECT(12)
        {
            Length             = (ushort)inBufferSize,
            DataIn             = NativeConstants.SCSI_IOCTL_DATA_IN,
            DataTransferLength = (uint)buffer.Length,
            DataBuffer         = (nint)data,
            TimeOutValue       = timeOutValue,
            Cdb =
            {
                [00] = 0xBE,                          // operation code: READ CD
                [01] = 0,                             // expected sector type: any
                [02] = (byte)(position >> 24 & 0xFF), // starting LBA
                [03] = (byte)(position >> 16 & 0xFF), // starting LBA
                [04] = (byte)(position >> 08 & 0xFF), // starting LBA
                [05] = (byte)(position >> 00 & 0xFF), // starting LBA
                [06] = (byte)(transfer >> 16 & 0xFF), // transfer length
                [07] = (byte)(transfer >> 08 & 0xFF), // transfer length
                [08] = (byte)(transfer >> 00 & 0xFF), // transfer length
                [09] = 0xF8,                          // sync, header, sub-header, user data, EDC, ECC
                [10] = 0,                             // sub-channel data: none
                [11] = 0,                             // control
            },
        };

        Marshal.StructureToPtr(sptd, inBuffer, false);

        var ioctl = NativeMethods.DeviceIoControl(
            Handle,
            NativeConstants.IOCTL_SCSI_PASS_THROUGH_DIRECT,
            inBuffer,
            inBufferSize,
            inBuffer,
            inBufferSize,
            out _,
            nativeOverlapped
        );

        Marshal.FreeHGlobal(inBuffer);

        if (ioctl is false && Marshal.GetLastPInvokeError() is not NativeConstants.ERROR_IO_PENDING)
        {
            throw new Win32Exception();
        }
    }

    var tcs = new TaskCompletionSource<ISector>();

    var rwh = ThreadPool.RegisterWaitForSingleObject(
        manualResetEvent,
        ReadSectorAsyncCallBack,
        (tcs, memory),
        TimeSpan.FromSeconds(timeOutValue),
        true
    );

    tcs.Task.ContinueWith(_ => rwh.Unregister(null), TaskScheduler.Current);

    return tcs.Task;
}

ReadSectorAsyncCallBack 回调

private void ReadSectorAsyncCallBack(object? state, bool timedOut)
{
    var (source, memory) = ((TaskCompletionSource<ISector>, NativeMemory<byte>))state!;

    try
    {
        if (timedOut)
        {
            source.SetCanceled();
        }
        else
        {
            ISector sector = Sector switch
            {
                SectorCooked2048       => throw new NotSupportedException(Sector.GetType().Name),
                SectorCooked2324       => throw new NotSupportedException(Sector.GetType().Name),
                SectorCooked2336       => throw new NotSupportedException(Sector.GetType().Name),
                SectorRawAudio         => MemoryMarshal.Read<SectorRawAudio>(memory.Span),
                SectorRawMode0         => MemoryMarshal.Read<SectorRawMode0>(memory.Span),
                SectorRawMode1         => MemoryMarshal.Read<SectorRawMode1>(memory.Span),
                SectorRawMode2Form1    => MemoryMarshal.Read<SectorRawMode2Form1>(memory.Span),
                SectorRawMode2Form2    => MemoryMarshal.Read<SectorRawMode2Form2>(memory.Span),
                SectorRawMode2FormLess => MemoryMarshal.Read<SectorRawMode2FormLess>(memory.Span),
                _                      => throw new NotSupportedException(Sector.GetType().Name),
            };

            source.SetResult(sector);
        }
    }
    catch (Exception e)
    {
        source.SetException(e);
    }
    finally
    {
        memory.Dispose();
    }
}

存在的问题与改进建议

  1. ManualResetEvent初始状态错误
    当前创建时用new ManualResetEvent(true),初始状态为已触发,会导致ThreadPool的等待逻辑立即执行回调,此时IO操作可能还未完成。应改为new ManualResetEvent(false),让事件初始处于未触发状态,直到IO完成后由系统触发。

  2. Overlapped对象未正确释放
    调用overlapped.Pack()后生成的nativeOverlapped必须手动释放,否则会造成内存泄漏。需要在回调中添加Overlapped.Unpack(nativeOverlapped)并调用NativeOverlapped.Free(nativeOverlapped),可以将nativeOverlapped作为状态参数的一部分传入回调。

  3. 超时逻辑不严谨
    当触发超时回调时,直接调用source.SetCanceled()并不准确——IO操作可能是因为其他错误而非超时完成。此时应该调用GetOverlappedResult获取实际的IO结果和错误码,再决定是设置取消、异常还是结果。

  4. TaskCompletionSource的状态保护
    虽然RegisterWaitForSingleObject的最后一个参数设为true(只触发一次),但仍需确保不会重复设置Task状态。可以使用TrySetXXX系列方法替代SetXXX,避免因竞态条件导致的异常。

改进后的代码示例

调整后的ReadSectorAsync

public override unsafe Task<ISector> ReadSectorAsync(in int index)
{
    // 初始状态设为未触发
    using var manualResetEvent = new ManualResetEvent(false);

    var overlapped = new Overlapped
    {
        EventHandleIntPtr = manualResetEvent.SafeWaitHandle.DangerousGetHandle(),
    };

    var nativeOverlapped = overlapped.Pack(null, null);

    var memory = Disc.GetDeviceAlignedBuffer(2352, Handle);

    var buffer = memory.Span;

    const int timeOutValue = 3;

    fixed (byte* data = &MemoryMarshal.GetReference(buffer))
    {
        var inBufferSize = (uint)Marshal.SizeOf<NativeTypes.SCSI_PASS_THROUGH_DIRECT>();
        var inBuffer = Marshal.AllocHGlobal((int)inBufferSize);

        try
        {
            var position = index;
            var transfer = 1u; // sectors

            var sptd = new NativeTypes.SCSI_PASS_THROUGH_DIRECT(12)
            {
                Length             = (ushort)inBufferSize,
                DataIn             = NativeConstants.SCSI_IOCTL_DATA_IN,
                DataTransferLength = (uint)buffer.Length,
                DataBuffer         = (nint)data,
                TimeOutValue       = timeOutValue,
                Cdb =
                {
                    [00] = 0xBE,                          // operation code: READ CD
                    [01] = 0,                             // expected sector type: any
                    [02] = (byte)(position >> 24 & 0xFF), // starting LBA
                    [03] = (byte)(position >> 16 & 0xFF), // starting LBA
                    [04] = (byte)(position >> 08 & 0xFF), // starting LBA
                    [05] = (byte)(position >> 00 & 0xFF), // starting LBA
                    [06] = (byte)(transfer >> 16 & 0xFF), // transfer length
                    [07] = (byte)(transfer >> 08 & 0xFF), // transfer length
                    [08] = (byte)(transfer >> 00 & 0xFF), // transfer length
                    [09] = 0xF8,                          // sync, header, sub-header, user data, EDC, ECC
                    [10] = 0,                             // sub-channel data: none
                    [11] = 0,                             // control
                },
            };

            Marshal.StructureToPtr(sptd, inBuffer, false);

            var ioctl = NativeMethods.DeviceIoControl(
                Handle,
                NativeConstants.IOCTL_SCSI_PASS_THROUGH_DIRECT,
                inBuffer,
                inBufferSize,
                inBuffer,
                inBufferSize,
                out _,
                nativeOverlapped
            );

            if (!ioctl)
            {
                var lastError = Marshal.GetLastPInvokeError();
                if (lastError != NativeConstants.ERROR_IO_PENDING)
                {
                    // 清理资源后抛出异常
                    Overlapped.Unpack(nativeOverlapped);
                    NativeOverlapped.Free(nativeOverlapped);
                    memory.Dispose();
                    throw new Win32Exception(lastError);
                }
            }
        }
        finally
        {
            Marshal.FreeHGlobal(inBuffer);
        }
    }

    var tcs = new TaskCompletionSource<ISector>();

    // 将nativeOverlapped加入状态参数,方便回调中释放
    var state = (tcs, memory, nativeOverlapped, Handle);

    var rwh = ThreadPool.RegisterWaitForSingleObject(
        manualResetEvent,
        ReadSectorAsyncCallBack,
        state,
        TimeSpan.FromSeconds(timeOutValue),
        true
    );

    tcs.Task.ContinueWith(_ => rwh.Unregister(null), TaskScheduler.Current);

    return tcs.Task;
}

调整后的ReadSectorAsyncCallBack

private unsafe void ReadSectorAsyncCallBack(object? state, bool timedOut)
{
    var (source, memory, nativeOverlapped, handle) = ((TaskCompletionSource<ISector>, NativeMemory<byte>, NativeOverlapped*, nint))state!;

    try
    {
        if (timedOut)
        {
            // 超时后尝试取消IO操作
            NativeMethods.CancelIo(handle);
            source.TrySetCanceled();
            return;
        }

        // 获取IO操作结果
        if (!NativeMethods.GetOverlappedResult(handle, nativeOverlapped, out var bytesTransferred, false))
        {
            var error = Marshal.GetLastPInvokeError();
            source.TrySetException(new Win32Exception(error));
            return;
        }

        // 验证传输字节数是否符合预期
        if (bytesTransferred != 2352)
        {
            source.TrySetException(new IOException($"读取扇区失败,实际传输字节数:{bytesTransferred}"));
            return;
        }

        ISector sector = Sector switch
        {
            SectorCooked2048       => throw new NotSupportedException(nameof(SectorCooked2048)),
            SectorCooked2324       => throw new NotSupportedException(nameof(SectorCooked2324)),
            SectorCooked2336       => throw new NotSupportedException(nameof(SectorCooked2336)),
            SectorRawAudio         => MemoryMarshal.Read<SectorRawAudio>(memory.Span),
            SectorRawMode0         => MemoryMarshal.Read<SectorRawMode0>(memory.Span),
            SectorRawMode1         => MemoryMarshal.Read<SectorRawMode1>(memory.Span),
            SectorRawMode2Form1    => MemoryMarshal.Read<SectorRawMode2Form1>(memory.Span),
            SectorRawMode2Form2    => MemoryMarshal.Read<SectorRawMode2Form2>(memory.Span),
            SectorRawMode2FormLess => MemoryMarshal.Read<SectorRawMode2FormLess>(memory.Span),
            _                      => throw new NotSupportedException(Sector.GetType().Name),
        };

        source.TrySetResult(sector);
    }
    catch (Exception e)
    {
        source.TrySetException(e);
    }
    finally
    {
        // 释放Overlapped资源
        Overlapped.Unpack(nativeOverlapped);
        NativeOverlapped.Free(nativeOverlapped);
        memory.Dispose();
    }
}

关键修复点说明

  • 修正ManualResetEvent初始状态,确保IO完成前不会触发回调
  • 添加NativeOverlapped的释放逻辑,避免内存泄漏
  • 使用GetOverlappedResult获取真实IO结果,替代单纯依赖超时标记
  • 改用TrySetXXX方法确保Task状态不会被重复设置
  • 增加传输字节数验证,提前发现读取异常

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 10:36:00