如何将DeviceIoControl异步操作正确转换为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(); } }
存在的问题与改进建议
ManualResetEvent初始状态错误
当前创建时用new ManualResetEvent(true),初始状态为已触发,会导致ThreadPool的等待逻辑立即执行回调,此时IO操作可能还未完成。应改为new ManualResetEvent(false),让事件初始处于未触发状态,直到IO完成后由系统触发。Overlapped对象未正确释放
调用overlapped.Pack()后生成的nativeOverlapped必须手动释放,否则会造成内存泄漏。需要在回调中添加Overlapped.Unpack(nativeOverlapped)并调用NativeOverlapped.Free(nativeOverlapped),可以将nativeOverlapped作为状态参数的一部分传入回调。超时逻辑不严谨
当触发超时回调时,直接调用source.SetCanceled()并不准确——IO操作可能是因为其他错误而非超时完成。此时应该调用GetOverlappedResult获取实际的IO结果和错误码,再决定是设置取消、异常还是结果。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

