Skip to content

Don't use callbacks when performing sync IO with async handles #126839

@adamsitnik

Description

@adamsitnik

In RandomAccess.Windows.cs, we create a dedicated callback:

private static readonly IOCompletionCallback s_callback = AllocateCallback();

Then provide it in these places:

NativeOverlapped* result = handle.ThreadPoolBinding!.UnsafeAllocateNativeOverlapped(s_callback, resetEvent, null);

And handle the callback in a dedicated class:

// We need to store the reference count (see the comment in ReleaseRefCount) and an EventHandle to signal the completion.
// We could keep these two things separate, but since ManualResetEvent is sealed and we want to avoid any extra allocations, this type has been created.
// It's basically ManualResetEvent with reference count.
private sealed class CallbackResetEvent : EventWaitHandle
{
private readonly ThreadPoolBoundHandle _threadPoolBoundHandle;
private int _freeWhenZero = 2; // one for the callback and another for the method that calls GetOverlappedResult
internal CallbackResetEvent(ThreadPoolBoundHandle threadPoolBoundHandle) : base(initialState: false, EventResetMode.ManualReset)
{
_threadPoolBoundHandle = threadPoolBoundHandle;
}
internal unsafe void ReleaseRefCount(NativeOverlapped* pOverlapped)
{
// Each SafeFileHandle opened for async IO is bound to ThreadPool.
// It requires us to provide a callback even if we want to use EventHandle and use GetOverlappedResult to obtain the result.
// There can be a race condition between the call to GetOverlappedResult and the callback invocation,
// so we need to track the number of references, and when it drops to zero, then free the native overlapped.
if (Interlocked.Decrement(ref _freeWhenZero) == 0)
{
_threadPoolBoundHandle.FreeNativeOverlapped(pOverlapped);
}
}
}

Based on my finding from #126807, we don't need to provide a callback when configuring WaitHandle for overlapped provided by ReadFile/WriteFile. What we need to do is setting the low-order bit of hEvent in the OVERLAPPED structure:

private static unsafe NativeOverlapped* AllocateOverlapped(EventWaitHandle waitHandle)
{
NativeOverlapped* overlapped = (NativeOverlapped*)NativeMemory.AllocZeroed((nuint)sizeof(NativeOverlapped));
overlapped->EventHandle = SetLowOrderBit(waitHandle);
return overlapped;
}

/// <summary>
/// Returns the event handle with the low-order bit set.
/// Per https://learn.microsoft.com/windows/win32/api/ioapiset/nf-ioapiset-getqueuedcompletionstatus,
/// setting the low-order bit of hEvent in the OVERLAPPED structure prevents the I/O completion
/// from being queued to a completion port bound to the same file object. The kernel masks off
/// the bit when signaling, so the event still works normally.
/// </summary>
private static nint SetLowOrderBit(EventWaitHandle waitHandle)
=> waitHandle.SafeWaitHandle.DangerousGetHandle() | 1;

This should allow us to get rid of CallbackResetEvent, slightly reduce allocations and complexity. The perf might also get better.

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions