Skip to content

Commit fae90a6

Browse files
authored
Implement alertable waits for WaitHandle to support APC handling in NativeAOT (#118256)
The implementation follows the same retry pattern used in CoreCLR's Thread::DoAppropriateWaitWorker, ensuring consistency across runtime implementations. Fixes #118233. * Disable RunTestInterruptInfiniteWait for NativeAOT due to unimplemented Thread.Interrupt Co-authored-by: copilot-swe-agent[bot] <[email protected]>
1 parent bb20ad0 commit fae90a6

File tree

3 files changed

+74
-12
lines changed

3 files changed

+74
-12
lines changed

src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ internal static partial class Interop
1010
internal static partial class Kernel32
1111
{
1212
internal const int WAIT_FAILED = unchecked((int)0xFFFFFFFF);
13+
internal const int WAIT_IO_COMPLETION = 0x000000C0;
1314

1415
[LibraryImport(Libraries.Kernel32)]
1516
internal static partial uint WaitForMultipleObjectsEx(uint nCount, IntPtr lpHandles, BOOL bWaitAll, uint dwMilliseconds, BOOL bAlertable);
1617

1718
[LibraryImport(Libraries.Kernel32)]
1819
internal static partial uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
1920

21+
[LibraryImport(Libraries.Kernel32)]
22+
internal static partial uint WaitForSingleObjectEx(IntPtr hHandle, uint dwMilliseconds, BOOL bAlertable);
23+
2024
[LibraryImport(Libraries.Kernel32)]
2125
internal static partial uint SignalObjectAndWait(IntPtr hObjectToSignal, IntPtr hObjectToWaitOn, uint dwMilliseconds, BOOL bAlertable);
2226

src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,46 @@ private static unsafe int WaitForMultipleObjectsIgnoringSyncContext(IntPtr* pHan
4949
Thread currentThread = Thread.CurrentThread;
5050
currentThread.SetWaitSleepJoinState();
5151

52-
#if NATIVEAOT
53-
int result;
54-
if (reentrantWait)
52+
long startTime = 0;
53+
if (millisecondsTimeout != -1)
5554
{
56-
Debug.Assert(!waitAll);
57-
result = RuntimeImports.RhCompatibleReentrantWaitAny(false, millisecondsTimeout, numHandles, pHandles);
55+
startTime = Environment.TickCount64;
5856
}
59-
else
57+
58+
int result;
59+
while (true)
6060
{
61-
result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.FALSE);
62-
}
61+
#if NATIVEAOT
62+
if (reentrantWait)
63+
{
64+
Debug.Assert(!waitAll);
65+
result = RuntimeImports.RhCompatibleReentrantWaitAny(true, millisecondsTimeout, numHandles, pHandles);
66+
}
67+
else
68+
{
69+
result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.TRUE);
70+
}
6371
#else
64-
int result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.FALSE);
72+
result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.TRUE);
6573
#endif
74+
75+
if (result != Interop.Kernel32.WAIT_IO_COMPLETION)
76+
break;
77+
78+
// Handle APC completion by adjusting timeout and retrying
79+
if (millisecondsTimeout != -1)
80+
{
81+
long currentTime = Environment.TickCount64;
82+
long elapsed = currentTime - startTime;
83+
if (elapsed >= millisecondsTimeout)
84+
{
85+
result = Interop.Kernel32.WAIT_TIMEOUT;
86+
break;
87+
}
88+
millisecondsTimeout -= (int)elapsed;
89+
startTime = currentTime;
90+
}
91+
}
6692
currentThread.ClearWaitSleepJoinState();
6793

6894
if (result == Interop.Kernel32.WAIT_FAILED)
@@ -102,8 +128,35 @@ private static int SignalAndWaitCore(IntPtr handleToSignal, IntPtr handleToWaitO
102128
{
103129
Debug.Assert(millisecondsTimeout >= -1);
104130

105-
int ret = (int)Interop.Kernel32.SignalObjectAndWait(handleToSignal, handleToWaitOn, (uint)millisecondsTimeout, Interop.BOOL.FALSE);
131+
long startTime = 0;
132+
if (millisecondsTimeout != -1)
133+
{
134+
startTime = Environment.TickCount64;
135+
}
136+
137+
// Signal the object and wait for the first time
138+
int ret = (int)Interop.Kernel32.SignalObjectAndWait(handleToSignal, handleToWaitOn, (uint)millisecondsTimeout, Interop.BOOL.TRUE);
139+
140+
// Handle APC completion by retrying with WaitForSingleObjectEx (without signaling again)
141+
while (ret == Interop.Kernel32.WAIT_IO_COMPLETION)
142+
{
143+
if (millisecondsTimeout != -1)
144+
{
145+
long currentTime = Environment.TickCount64;
146+
long elapsed = currentTime - startTime;
106147

148+
if (elapsed >= millisecondsTimeout)
149+
{
150+
ret = Interop.Kernel32.WAIT_TIMEOUT;
151+
break;
152+
}
153+
millisecondsTimeout -= (int)elapsed;
154+
startTime = currentTime;
155+
}
156+
157+
// For retries, only wait on the handle (don't signal again)
158+
ret = (int)Interop.Kernel32.WaitForSingleObjectEx(handleToWaitOn, (uint)millisecondsTimeout, Interop.BOOL.TRUE);
159+
}
107160
if (ret == Interop.Kernel32.WAIT_FAILED)
108161
{
109162
ThrowWaitFailedException(Interop.Kernel32.GetLastError());

src/tests/baseservices/threading/regressions/115178/115178.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,12 +284,17 @@ private static void RunTestInterruptInfiniteWait()
284284
}
285285

286286
[ConditionalFact(typeof(TestLibrary.Utilities), nameof(TestLibrary.Utilities.IsWindows))]
287-
[ActiveIssue("https://github.com/dotnet/runtime/issues/118233", typeof(TestLibrary.Utilities), nameof(TestLibrary.Utilities.IsNativeAot))]
288287
public static int TestEntryPoint()
289288
{
290289
RunTestUsingInfiniteWait();
291290
RunTestUsingTimedWait();
292-
RunTestInterruptInfiniteWait();
291+
292+
// Thread.Interrupt is not implemented on NativeAOT - https://github.com/dotnet/runtime/issues/69919
293+
if (!TestLibrary.Utilities.IsNativeAot)
294+
{
295+
RunTestInterruptInfiniteWait();
296+
}
297+
293298
return result;
294299
}
295300
}

0 commit comments

Comments
 (0)