From e4d13d1a1abc00cf1c8ddaf78c629e73e64f89b5 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 21 Aug 2025 12:22:51 -0700 Subject: [PATCH 01/10] Implement Thread.Interrupt on NativeAOT on Windows --- src/coreclr/nativeaot/Runtime/thread.cpp | 50 ++++++ src/coreclr/nativeaot/Runtime/thread.h | 6 +- .../src/System/Runtime/RuntimeImports.cs | 8 + .../Threading/Thread.NativeAot.Windows.cs | 157 +++++++++++++++++- .../src/System/Threading/Thread.NativeAot.cs | 1 + .../Windows/Kernel32/Interop.Threading.cs | 7 + .../src/System/Threading/Thread.Windows.cs | 2 +- .../System/Threading/WaitHandle.Windows.cs | 10 ++ .../tests/ThreadTests.cs | 4 +- .../System.Threading/tests/MonitorTests.cs | 1 - .../threading/regressions/115178/115178.cs | 7 +- 11 files changed, 234 insertions(+), 19 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/thread.cpp b/src/coreclr/nativeaot/Runtime/thread.cpp index 45b88d1cde2def..e01d5c94077516 100644 --- a/src/coreclr/nativeaot/Runtime/thread.cpp +++ b/src/coreclr/nativeaot/Runtime/thread.cpp @@ -1160,6 +1160,23 @@ bool Thread::CheckPendingRedirect(PCODE eip) #endif // TARGET_X86 +void Thread::SetInterrupted(bool isInterrupted) +{ + if (isInterrupted) + { + SetState(TSF_Interrupted); + } + else + { + ClearState(TSF_Interrupted); + } +} + +bool Thread::CheckInterrupted() +{ + return IsStateSet(TSF_Interrupted); +} + #endif // !DACCESS_COMPILE void Thread::ValidateExInfoStack() @@ -1323,6 +1340,39 @@ FCIMPL0(size_t, RhGetDefaultStackSize) } FCIMPLEND +#ifdef TARGET_WINDOWS +// Native APC callback for Thread.Interrupt +// This callback sets the interrupt flag on the current thread +static VOID CALLBACK InterruptApcCallback(ULONG_PTR /* parameter */) +{ + // Set the interrupt flag on the current thread + Thread* pCurrentThread = ThreadStore::RawGetCurrentThread(); + if (pCurrentThread != nullptr) + { + pCurrentThread->SetInterrupted(true); + } +} + +// Function to get the address of the interrupt APC callback +FCIMPL0(void*, RhGetInterruptApcCallback) +{ + return (void*)InterruptApcCallback; +} +FCIMPLEND + +FCIMPL0(FC_BOOL_RET, RhCheckAndClearPendingInterrupt) +{ + Thread* pCurrentThread = ThreadStore::RawGetCurrentThread(); + if (pCurrentThread != nullptr && pCurrentThread->CheckInterrupted()) + { + pCurrentThread->SetInterrupted(false); + FC_RETURN_BOOL(true); + } + FC_RETURN_BOOL(false); +} +FCIMPLEND +#endif // TARGET_WINDOWS + // Standard calling convention variant and actual implementation for RhpReversePInvokeAttachOrTrapThread EXTERN_C NOINLINE void FASTCALL RhpReversePInvokeAttachOrTrapThread2(ReversePInvokeFrame* pFrame) { diff --git a/src/coreclr/nativeaot/Runtime/thread.h b/src/coreclr/nativeaot/Runtime/thread.h index 83249cfc6bc77e..a4ca462d78d9c9 100644 --- a/src/coreclr/nativeaot/Runtime/thread.h +++ b/src/coreclr/nativeaot/Runtime/thread.h @@ -132,7 +132,7 @@ struct ee_alloc_context struct RuntimeThreadLocals { - ee_alloc_context m_eeAllocContext; + ee_alloc_context m_eeAllocContext; uint32_t volatile m_ThreadStateFlags; // see Thread::ThreadStateFlags enum PInvokeTransitionFrame* m_pTransitionFrame; PInvokeTransitionFrame* m_pDeferredTransitionFrame; // see Thread::EnablePreemptiveMode @@ -214,6 +214,7 @@ class Thread : private RuntimeThreadLocals // // On Unix this is an optimization to not queue up more signals when one is // still being processed. + TSF_Interrupted = 0x00000200, // Set to indicate Thread.Interrupt() has been called on this thread }; private: @@ -390,6 +391,9 @@ class Thread : private RuntimeThreadLocals void SetPendingRedirect(PCODE eip); bool CheckPendingRedirect(PCODE eip); #endif + + void SetInterrupted(bool isInterrupted); + bool CheckInterrupted(); }; #ifndef __GCENV_BASE_INCLUDED__ diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs index 0a37079dfa0e9d..5375713f18770c 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/RuntimeImports.cs @@ -607,6 +607,14 @@ internal static IntPtr RhGetModuleSection(TypeManagerHandle module, ReadyToRunSe [RuntimeImport(RuntimeLibrary, "RhGetDefaultStackSize")] internal static extern unsafe IntPtr RhGetDefaultStackSize(); + [MethodImplAttribute(MethodImplOptions.InternalCall)] + [RuntimeImport(RuntimeLibrary, "RhGetInterruptApcCallback")] + internal static extern unsafe delegate* unmanaged RhGetInterruptApcCallback(); + + [MethodImplAttribute(MethodImplOptions.InternalCall)] + [RuntimeImport(RuntimeLibrary, "RhCheckAndClearPendingInterrupt")] + internal static extern bool RhCheckAndClearPendingInterrupt(); + [MethodImplAttribute(MethodImplOptions.InternalCall)] [RuntimeImport("*", "RhGetCurrentThunkContext")] internal static extern IntPtr GetCurrentInteropThunkContext(); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index f01fe0f86f38f7..c6110bd4338190 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -26,6 +26,70 @@ public sealed partial class Thread partial void PlatformSpecificInitialize(); + internal static void SleepInternal(int millisecondsTimeout) + { + Debug.Assert(millisecondsTimeout >= -1); + + Thread currentThread = CurrentThread; + if (millisecondsTimeout == 0) + { + Thread.CheckForPendingInterrupt(); + UninterruptibleSleep0(); + return; + } + + if (millisecondsTimeout == -1) + { + // Infinite wait - use alertable wait + currentThread.SetWaitSleepJoinState(); + try + { + uint result; + do + { + Thread.CheckForPendingInterrupt(); + result = Interop.Kernel32.SleepEx(uint.MaxValue, true); + if (result == Interop.Kernel32.WAIT_IO_COMPLETION) + { + // Check if this was our interrupt APC + Thread.CheckForPendingInterrupt(); + } + } while (result == Interop.Kernel32.WAIT_IO_COMPLETION); + } + finally + { + currentThread.ClearWaitSleepJoinState(); + } + } + else + { + // Timed wait - use alertable wait + currentThread.SetWaitSleepJoinState(); + uint startTicks = (uint)Environment.TickCount; + uint remainingTimeout = (uint)millisecondsTimeout; + try + { + uint result; + do + { + Thread.CheckForPendingInterrupt(); + result = Interop.Kernel32.SleepEx(remainingTimeout, true); + if (result == Interop.Kernel32.WAIT_IO_COMPLETION) + { + // Check if this was our interrupt APC + Thread.CheckForPendingInterrupt(); + uint elapsed = (uint)Environment.TickCount - startTicks; + remainingTimeout = elapsed >= (uint)millisecondsTimeout ? 0 : (uint)millisecondsTimeout - elapsed; + } + } while (result == Interop.Kernel32.WAIT_IO_COMPLETION && remainingTimeout > 0); + } + finally + { + currentThread.ClearWaitSleepJoinState(); + } + } + } + // Platform-specific initialization of foreign threads, i.e. threads not created by Thread.Start private void PlatformSpecificInitializeExistingThread() { @@ -154,18 +218,56 @@ private bool JoinInternal(int millisecondsTimeout) try { - int result; - if (millisecondsTimeout == 0) { - result = (int)Interop.Kernel32.WaitForSingleObject(waitHandle.DangerousGetHandle(), 0); + int result = (int)Interop.Kernel32.WaitForSingleObject(waitHandle.DangerousGetHandle(), 0); + return result == (int)Interop.Kernel32.WAIT_OBJECT_0; } else { - result = WaitHandle.WaitOneCore(waitHandle.DangerousGetHandle(), millisecondsTimeout, useTrivialWaits: false); + Thread currentThread = CurrentThread; + currentThread.SetWaitSleepJoinState(); + try + { + uint result; + if (millisecondsTimeout == -1) + { + // Infinite wait + do + { + Thread.CheckForPendingInterrupt(); + result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), uint.MaxValue, Interop.BOOL.TRUE); + if (result == Interop.Kernel32.WAIT_IO_COMPLETION) + { + // Check if this was our interrupt APC + Thread.CheckForPendingInterrupt(); + } + } while (result == Interop.Kernel32.WAIT_IO_COMPLETION); + } + else + { + uint startTicks = (uint)Environment.TickCount; + uint remainingTimeout = (uint)millisecondsTimeout; + do + { + Thread.CheckForPendingInterrupt(); + result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), remainingTimeout, Interop.BOOL.TRUE); + if (result == Interop.Kernel32.WAIT_IO_COMPLETION) + { + // Check if this was our interrupt APC + Thread.CheckForPendingInterrupt(); + uint elapsed = (uint)Environment.TickCount - startTicks; + remainingTimeout = elapsed >= (uint)millisecondsTimeout ? 0 : (uint)millisecondsTimeout - elapsed; + } + } while (result == Interop.Kernel32.WAIT_IO_COMPLETION && remainingTimeout > 0); + } + return result == (int)Interop.Kernel32.WAIT_OBJECT_0; + } + finally + { + currentThread.ClearWaitSleepJoinState(); + } } - - return result == (int)Interop.Kernel32.WAIT_OBJECT_0; } finally { @@ -212,6 +314,13 @@ private unsafe bool CreateThread(GCHandle thisThreadHandle) // CoreCLR ignores OS errors while setting the priority, so do we SetPriorityLive(_priority); + // If the thread was interrupted before it was started, queue the interruption now + if (GetThreadStateBit(Interrupted)) + { + ClearThreadStateBit(Interrupted); + Interrupt(); + } + Interop.Kernel32.ResumeThread(_osHandle); return true; } @@ -393,7 +502,41 @@ internal static Thread EnsureThreadPoolThreadInitialized() return InitializeExistingThreadPoolThread(); } - public void Interrupt() { throw new PlatformNotSupportedException(); } + public void Interrupt() + { + using (_lock.EnterScope()) + { + // Thread.Interrupt for dead thread should do nothing + if (IsDead()) + { + return; + } + + // Thread.Interrupt for thread that has not been started yet should queue a pending interrupt + if (GetThreadStateBit(ThreadState.Unstarted)) + { + SetThreadStateBit(Interrupted); + return; + } + + // Queue APC to interrupt the target thread + if (!_osHandle.IsInvalid) + { + unsafe + { + Interop.Kernel32.QueueUserAPC(RuntimeImports.RhGetInterruptApcCallback(), _osHandle, 0); + } + } + } + } + + internal static void CheckForPendingInterrupt() + { + if (RuntimeImports.RhCheckAndClearPendingInterrupt()) + { + throw new ThreadInterruptedException(); + } + } internal static bool ReentrantWaitsEnabled => GetCurrentApartmentType() == ApartmentType.STA; diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs index 3758fca9e8a0a2..77106f368ed274 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs @@ -17,6 +17,7 @@ public sealed partial class Thread { // Extra bits used in _threadState private const ThreadState ThreadPoolThread = (ThreadState)0x1000; + private const ThreadState Interrupted = (ThreadState)0x2000; // Bits of _threadState that are returned by the ThreadState property private const ThreadState PublicThreadStateMask = (ThreadState)0x1FF; diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs index b323cacc6bcaee..8c6b2169146797 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs @@ -78,5 +78,12 @@ internal enum ThreadPriority : int [LibraryImport(Libraries.Kernel32, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool GetThreadIOPendingFlag(nint hThread, out BOOL lpIOIsPending); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static unsafe partial bool QueueUserAPC(delegate* unmanaged pfnAPC, SafeHandle hThread, nuint dwData); + + [LibraryImport(Libraries.Kernel32)] + internal static partial uint SleepEx(uint dwMilliseconds, [MarshalAs(UnmanagedType.Bool)] bool bAlertable); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Windows.cs index b9b9a0e16f9289..3d16397d7301fb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.Windows.cs @@ -18,7 +18,7 @@ public sealed partial class Thread { internal static void UninterruptibleSleep0() => Interop.Kernel32.Sleep(0); -#if !CORECLR +#if MONO private static void SleepInternal(int millisecondsTimeout) { Debug.Assert(millisecondsTimeout >= -1); diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs index 26ab6b4278b2bf..e682af3725abf5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs @@ -75,6 +75,11 @@ private static unsafe int WaitForMultipleObjectsIgnoringSyncContext(IntPtr* pHan if (result != Interop.Kernel32.WAIT_IO_COMPLETION) break; +#if !MONO + // Check for thread interrupt when APC completion occurs + Thread.CheckForPendingInterrupt(); +#endif + // Handle APC completion by adjusting timeout and retrying if (millisecondsTimeout != -1) { @@ -140,6 +145,11 @@ private static int SignalAndWaitCore(IntPtr handleToSignal, IntPtr handleToWaitO // Handle APC completion by retrying with WaitForSingleObjectEx (without signaling again) while (ret == Interop.Kernel32.WAIT_IO_COMPLETION) { +#if !MONO + // Check for thread interrupt when APC completion occurs + Thread.CheckForPendingInterrupt(); +#endif + if (millisecondsTimeout != -1) { long currentTime = Environment.TickCount64; diff --git a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs index 908d690e7e207d..972056094a5fff 100644 --- a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs +++ b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs @@ -749,7 +749,7 @@ public static void AbortSuspendTest() verify(); e.Set(); - waitForThread(); + waitForThread(); } private static void VerifyLocalDataSlot(LocalDataStoreSlot slot) @@ -916,7 +916,6 @@ public static void LocalDataSlotTest() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/69919", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] public static void InterruptTest() { // Interrupting a thread that is not blocked does not do anything, but once the thread starts blocking, it gets @@ -966,7 +965,6 @@ public static void InterruptTest() } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/69919", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public static void InterruptInFinallyBlockTest_SkipOnDesktopFramework() { diff --git a/src/libraries/System.Threading/tests/MonitorTests.cs b/src/libraries/System.Threading/tests/MonitorTests.cs index 13cb675cef34ae..1153330d0ec7e1 100644 --- a/src/libraries/System.Threading/tests/MonitorTests.cs +++ b/src/libraries/System.Threading/tests/MonitorTests.cs @@ -491,7 +491,6 @@ public static void ObjectHeaderSyncBlockTransitionTryEnterRaceTest() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/49521", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] [ActiveIssue("https://github.com/dotnet/runtime/issues/87718", TestRuntimes.Mono)] - [ActiveIssue("https://github.com/dotnet/runtimelab/issues/155", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] public static void InterruptWaitTest() { object obj = new(); diff --git a/src/tests/baseservices/threading/regressions/115178/115178.cs b/src/tests/baseservices/threading/regressions/115178/115178.cs index 0367f81c70df70..0a019821189361 100644 --- a/src/tests/baseservices/threading/regressions/115178/115178.cs +++ b/src/tests/baseservices/threading/regressions/115178/115178.cs @@ -288,12 +288,7 @@ public static int TestEntryPoint() { RunTestUsingInfiniteWait(); RunTestUsingTimedWait(); - - // Thread.Interrupt is not implemented on NativeAOT - https://github.com/dotnet/runtime/issues/69919 - if (!TestLibrary.Utilities.IsNativeAot) - { - RunTestInterruptInfiniteWait(); - } + RunTestInterruptInfiniteWait(); return result; } From 3b9a6d48fd3263797b271ee4fbc6e98159e38d74 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 21 Aug 2025 12:26:42 -0700 Subject: [PATCH 02/10] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/System/Threading/Thread.NativeAot.Windows.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index c6110bd4338190..3fa858b3b6d867 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -79,7 +79,8 @@ internal static void SleepInternal(int millisecondsTimeout) // Check if this was our interrupt APC Thread.CheckForPendingInterrupt(); uint elapsed = (uint)Environment.TickCount - startTicks; - remainingTimeout = elapsed >= (uint)millisecondsTimeout ? 0 : (uint)millisecondsTimeout - elapsed; + int elapsed = Environment.TickCount - startTicks; + remainingTimeout = Math.Max(0, millisecondsTimeout - elapsed); } } while (result == Interop.Kernel32.WAIT_IO_COMPLETION && remainingTimeout > 0); } @@ -257,7 +258,8 @@ private bool JoinInternal(int millisecondsTimeout) // Check if this was our interrupt APC Thread.CheckForPendingInterrupt(); uint elapsed = (uint)Environment.TickCount - startTicks; - remainingTimeout = elapsed >= (uint)millisecondsTimeout ? 0 : (uint)millisecondsTimeout - elapsed; + int elapsed = Environment.TickCount - startTicks; + remainingTimeout = elapsed >= millisecondsTimeout ? 0 : millisecondsTimeout - elapsed; } } while (result == Interop.Kernel32.WAIT_IO_COMPLETION && remainingTimeout > 0); } From fbdaa71bcacd8e134fda3fd9b1c26e962aa50172 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 21 Aug 2025 12:29:46 -0700 Subject: [PATCH 03/10] Cleanup --- .../Threading/Thread.NativeAot.Windows.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index 3fa858b3b6d867..a4972bd89cd27e 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -33,7 +33,7 @@ internal static void SleepInternal(int millisecondsTimeout) Thread currentThread = CurrentThread; if (millisecondsTimeout == 0) { - Thread.CheckForPendingInterrupt(); + CheckForPendingInterrupt(); UninterruptibleSleep0(); return; } @@ -47,12 +47,11 @@ internal static void SleepInternal(int millisecondsTimeout) uint result; do { - Thread.CheckForPendingInterrupt(); result = Interop.Kernel32.SleepEx(uint.MaxValue, true); if (result == Interop.Kernel32.WAIT_IO_COMPLETION) { // Check if this was our interrupt APC - Thread.CheckForPendingInterrupt(); + CheckForPendingInterrupt(); } } while (result == Interop.Kernel32.WAIT_IO_COMPLETION); } @@ -72,15 +71,13 @@ internal static void SleepInternal(int millisecondsTimeout) uint result; do { - Thread.CheckForPendingInterrupt(); result = Interop.Kernel32.SleepEx(remainingTimeout, true); if (result == Interop.Kernel32.WAIT_IO_COMPLETION) { // Check if this was our interrupt APC - Thread.CheckForPendingInterrupt(); - uint elapsed = (uint)Environment.TickCount - startTicks; - int elapsed = Environment.TickCount - startTicks; - remainingTimeout = Math.Max(0, millisecondsTimeout - elapsed); + CheckForPendingInterrupt(); + int elapsed = Environment.TickCount - (int)startTicks; + remainingTimeout = (uint)Math.Max(0, millisecondsTimeout - elapsed); } } while (result == Interop.Kernel32.WAIT_IO_COMPLETION && remainingTimeout > 0); } @@ -236,12 +233,11 @@ private bool JoinInternal(int millisecondsTimeout) // Infinite wait do { - Thread.CheckForPendingInterrupt(); result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), uint.MaxValue, Interop.BOOL.TRUE); if (result == Interop.Kernel32.WAIT_IO_COMPLETION) { // Check if this was our interrupt APC - Thread.CheckForPendingInterrupt(); + CheckForPendingInterrupt(); } } while (result == Interop.Kernel32.WAIT_IO_COMPLETION); } @@ -251,15 +247,13 @@ private bool JoinInternal(int millisecondsTimeout) uint remainingTimeout = (uint)millisecondsTimeout; do { - Thread.CheckForPendingInterrupt(); result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), remainingTimeout, Interop.BOOL.TRUE); if (result == Interop.Kernel32.WAIT_IO_COMPLETION) { // Check if this was our interrupt APC - Thread.CheckForPendingInterrupt(); - uint elapsed = (uint)Environment.TickCount - startTicks; - int elapsed = Environment.TickCount - startTicks; - remainingTimeout = elapsed >= millisecondsTimeout ? 0 : millisecondsTimeout - elapsed; + CheckForPendingInterrupt(); + int elapsed = Environment.TickCount - (int)startTicks; + remainingTimeout = (uint)Math.Max(0, millisecondsTimeout - elapsed); } } while (result == Interop.Kernel32.WAIT_IO_COMPLETION && remainingTimeout > 0); } From 4d9009a7934f045f03e4cf601ee82d545f3fae19 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 21 Aug 2025 12:39:12 -0700 Subject: [PATCH 04/10] Structure the loops like the re-entrant wait handling, insert a try-finally block around the re-entrant wait loop --- .../Threading/Thread.NativeAot.Windows.cs | 77 +++++++++++-------- .../System/Threading/WaitHandle.Windows.cs | 58 +++++++------- 2 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index a4972bd89cd27e..826cbac374e8f6 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -45,15 +45,15 @@ internal static void SleepInternal(int millisecondsTimeout) try { uint result; - do + while (true) { result = Interop.Kernel32.SleepEx(uint.MaxValue, true); - if (result == Interop.Kernel32.WAIT_IO_COMPLETION) + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) { - // Check if this was our interrupt APC - CheckForPendingInterrupt(); + break; } - } while (result == Interop.Kernel32.WAIT_IO_COMPLETION); + CheckForPendingInterrupt(); + } } finally { @@ -64,22 +64,30 @@ internal static void SleepInternal(int millisecondsTimeout) { // Timed wait - use alertable wait currentThread.SetWaitSleepJoinState(); - uint startTicks = (uint)Environment.TickCount; - uint remainingTimeout = (uint)millisecondsTimeout; + long startTime = Environment.TickCount64; try { uint result; - do + while (true) { - result = Interop.Kernel32.SleepEx(remainingTimeout, true); - if (result == Interop.Kernel32.WAIT_IO_COMPLETION) + result = Interop.Kernel32.SleepEx((uint)millisecondsTimeout, true); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) { - // Check if this was our interrupt APC - CheckForPendingInterrupt(); - int elapsed = Environment.TickCount - (int)startTicks; - remainingTimeout = (uint)Math.Max(0, millisecondsTimeout - elapsed); + break; } - } while (result == Interop.Kernel32.WAIT_IO_COMPLETION && remainingTimeout > 0); + // Check if this was our interrupt APC + CheckForPendingInterrupt(); + // Handle APC completion by adjusting timeout and retrying + long currentTime = Environment.TickCount64; + long elapsed = currentTime - startTime; + if (elapsed >= millisecondsTimeout) + { + result = Interop.Kernel32.WAIT_TIMEOUT; + break; + } + millisecondsTimeout -= (int)elapsed; + startTime = currentTime; + } } finally { @@ -231,31 +239,40 @@ private bool JoinInternal(int millisecondsTimeout) if (millisecondsTimeout == -1) { // Infinite wait - do + while (true) { result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), uint.MaxValue, Interop.BOOL.TRUE); - if (result == Interop.Kernel32.WAIT_IO_COMPLETION) + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) { - // Check if this was our interrupt APC - CheckForPendingInterrupt(); + break; } - } while (result == Interop.Kernel32.WAIT_IO_COMPLETION); + // Check if this was our interrupt APC + CheckForPendingInterrupt(); + } } else { - uint startTicks = (uint)Environment.TickCount; - uint remainingTimeout = (uint)millisecondsTimeout; - do + long startTime = Environment.TickCount64; + while (true) { - result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), remainingTimeout, Interop.BOOL.TRUE); - if (result == Interop.Kernel32.WAIT_IO_COMPLETION) + result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), (uint)millisecondsTimeout, Interop.BOOL.TRUE); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) { - // Check if this was our interrupt APC - CheckForPendingInterrupt(); - int elapsed = Environment.TickCount - (int)startTicks; - remainingTimeout = (uint)Math.Max(0, millisecondsTimeout - elapsed); + break; } - } while (result == Interop.Kernel32.WAIT_IO_COMPLETION && remainingTimeout > 0); + // Check if this was our interrupt APC + CheckForPendingInterrupt(); + // Handle APC completion by adjusting timeout and retrying + long currentTime = Environment.TickCount64; + long elapsed = currentTime - startTime; + if (elapsed >= millisecondsTimeout) + { + result = Interop.Kernel32.WAIT_TIMEOUT; + break; + } + millisecondsTimeout -= (int)elapsed; + startTime = currentTime; + } } return result == (int)Interop.Kernel32.WAIT_OBJECT_0; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs index e682af3725abf5..9237a093735106 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs @@ -56,45 +56,51 @@ private static unsafe int WaitForMultipleObjectsIgnoringSyncContext(IntPtr* pHan } int result; - while (true) + try { -#if NATIVEAOT - if (reentrantWait) - { - Debug.Assert(!waitAll); - result = RuntimeImports.RhCompatibleReentrantWaitAny(true, millisecondsTimeout, numHandles, pHandles); - } - else + while (true) { - result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.TRUE); - } +#if NATIVEAOT + if (reentrantWait) + { + Debug.Assert(!waitAll); + result = RuntimeImports.RhCompatibleReentrantWaitAny(true, millisecondsTimeout, numHandles, pHandles); + } + else + { + result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.TRUE); + } #else - result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.TRUE); + result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.TRUE); #endif - if (result != Interop.Kernel32.WAIT_IO_COMPLETION) - break; + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) + break; #if !MONO - // Check for thread interrupt when APC completion occurs - Thread.CheckForPendingInterrupt(); + // Check for thread interrupt when APC completion occurs + Thread.CheckForPendingInterrupt(); #endif - // Handle APC completion by adjusting timeout and retrying - if (millisecondsTimeout != -1) - { - long currentTime = Environment.TickCount64; - long elapsed = currentTime - startTime; - if (elapsed >= millisecondsTimeout) + // Handle APC completion by adjusting timeout and retrying + if (millisecondsTimeout != -1) { - result = Interop.Kernel32.WAIT_TIMEOUT; - break; + long currentTime = Environment.TickCount64; + long elapsed = currentTime - startTime; + if (elapsed >= millisecondsTimeout) + { + result = Interop.Kernel32.WAIT_TIMEOUT; + break; + } + millisecondsTimeout -= (int)elapsed; + startTime = currentTime; } - millisecondsTimeout -= (int)elapsed; - startTime = currentTime; } } - currentThread.ClearWaitSleepJoinState(); + finally + { + currentThread.ClearWaitSleepJoinState(); + } if (result == Interop.Kernel32.WAIT_FAILED) { From 68ad43489324da6c466e94be8840b8ff88dd0524 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 21 Aug 2025 13:10:41 -0700 Subject: [PATCH 05/10] Add ifdef around flag only used on Windows --- .../src/System/Threading/Thread.NativeAot.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs index 77106f368ed274..34db94ad986451 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs @@ -17,7 +17,9 @@ public sealed partial class Thread { // Extra bits used in _threadState private const ThreadState ThreadPoolThread = (ThreadState)0x1000; +#if TARGET_WINDOWS private const ThreadState Interrupted = (ThreadState)0x2000; +#endif // Bits of _threadState that are returned by the ThreadState property private const ThreadState PublicThreadStateMask = (ThreadState)0x1FF; From 17eca2b8fec6ffa7d1dce4a7c97c0ed8715b1e8c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 21 Aug 2025 14:34:18 -0700 Subject: [PATCH 06/10] Check interrupt before we wait (in case the APC ran at thread startup). Change check to assert. --- .../src/System/Threading/Thread.NativeAot.Windows.cs | 12 +++++------- .../src/System/Threading/WaitHandle.Windows.cs | 10 ++++++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index 826cbac374e8f6..69f82782b81ff2 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -30,10 +30,11 @@ internal static void SleepInternal(int millisecondsTimeout) { Debug.Assert(millisecondsTimeout >= -1); + CheckForPendingInterrupt(); + Thread currentThread = CurrentThread; if (millisecondsTimeout == 0) { - CheckForPendingInterrupt(); UninterruptibleSleep0(); return; } @@ -532,13 +533,10 @@ public void Interrupt() return; } - // Queue APC to interrupt the target thread - if (!_osHandle.IsInvalid) + Debug.Assert(!_osHandle.IsInvalid); + unsafe { - unsafe - { - Interop.Kernel32.QueueUserAPC(RuntimeImports.RhGetInterruptApcCallback(), _osHandle, 0); - } + Interop.Kernel32.QueueUserAPC(RuntimeImports.RhGetInterruptApcCallback(), _osHandle, 0); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs index 9237a093735106..9a9f8227ab8c57 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs @@ -58,6 +58,11 @@ private static unsafe int WaitForMultipleObjectsIgnoringSyncContext(IntPtr* pHan int result; try { +#if !MONO + // Check for thread interrupt when APC completion occurs + Thread.CheckForPendingInterrupt(); +#endif + while (true) { #if NATIVEAOT @@ -145,6 +150,11 @@ private static int SignalAndWaitCore(IntPtr handleToSignal, IntPtr handleToWaitO startTime = Environment.TickCount64; } +#if !MONO + // Check for thread interrupt when APC completion occurs + Thread.CheckForPendingInterrupt(); +#endif + // Signal the object and wait for the first time int ret = (int)Interop.Kernel32.SignalObjectAndWait(handleToSignal, handleToWaitOn, (uint)millisecondsTimeout, Interop.BOOL.TRUE); From 5a541a767ff4703d7a3c0c5a426de6cc520f829d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 21 Aug 2025 15:15:52 -0700 Subject: [PATCH 07/10] Update Thread.NativeAot.Windows.cs Co-authored-by: Jan Kotas --- .../src/System/Threading/Thread.NativeAot.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index 69f82782b81ff2..e45976bcc5ebb9 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -32,13 +32,13 @@ internal static void SleepInternal(int millisecondsTimeout) CheckForPendingInterrupt(); - Thread currentThread = CurrentThread; if (millisecondsTimeout == 0) { UninterruptibleSleep0(); return; } + Thread currentThread = CurrentThread; if (millisecondsTimeout == -1) { // Infinite wait - use alertable wait From d661e564af00541d05c7f8b6c30f29274185d704 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 22 Aug 2025 11:37:19 -0700 Subject: [PATCH 08/10] Use Timeout constants and move state clearing to be manually done before throwing the exception instead of in a finally block --- .../Threading/Thread.NativeAot.Windows.cs | 141 ++++++++---------- .../System/Threading/WaitHandle.Windows.cs | 74 ++++----- .../src/System/Threading/Thread.Mono.cs | 6 + 3 files changed, 94 insertions(+), 127 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index e45976bcc5ebb9..bf5c25c781603d 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -28,72 +28,54 @@ public sealed partial class Thread internal static void SleepInternal(int millisecondsTimeout) { - Debug.Assert(millisecondsTimeout >= -1); + Debug.Assert(millisecondsTimeout >= Timeout.Infinite); CheckForPendingInterrupt(); - if (millisecondsTimeout == 0) - { - UninterruptibleSleep0(); - return; - } - Thread currentThread = CurrentThread; - if (millisecondsTimeout == -1) + if (millisecondsTimeout == Timeout.Infinite) { // Infinite wait - use alertable wait currentThread.SetWaitSleepJoinState(); - try + uint result; + while (true) { - uint result; - while (true) + result = Interop.Kernel32.SleepEx(Timeout.UnsignedInfinite, true); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) { - result = Interop.Kernel32.SleepEx(uint.MaxValue, true); - if (result != Interop.Kernel32.WAIT_IO_COMPLETION) - { - break; - } - CheckForPendingInterrupt(); + break; } + CheckForPendingInterrupt(); } - finally - { - currentThread.ClearWaitSleepJoinState(); - } + + currentThread.ClearWaitSleepJoinState(); } else { // Timed wait - use alertable wait currentThread.SetWaitSleepJoinState(); long startTime = Environment.TickCount64; - try + while (true) { - uint result; - while (true) + uint result = Interop.Kernel32.SleepEx((uint)millisecondsTimeout, true); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) { - result = Interop.Kernel32.SleepEx((uint)millisecondsTimeout, true); - if (result != Interop.Kernel32.WAIT_IO_COMPLETION) - { - break; - } - // Check if this was our interrupt APC - CheckForPendingInterrupt(); - // Handle APC completion by adjusting timeout and retrying - long currentTime = Environment.TickCount64; - long elapsed = currentTime - startTime; - if (elapsed >= millisecondsTimeout) - { - result = Interop.Kernel32.WAIT_TIMEOUT; - break; - } - millisecondsTimeout -= (int)elapsed; - startTime = currentTime; + break; } + // Check if this was our interrupt APC + CheckForPendingInterrupt(); + // Handle APC completion by adjusting timeout and retrying + long currentTime = Environment.TickCount64; + long elapsed = currentTime - startTime; + if (elapsed >= millisecondsTimeout) + { + break; + } + millisecondsTimeout -= (int)elapsed; + startTime = currentTime; } - finally - { - currentThread.ClearWaitSleepJoinState(); - } + + currentThread.ClearWaitSleepJoinState(); } } @@ -234,53 +216,47 @@ private bool JoinInternal(int millisecondsTimeout) { Thread currentThread = CurrentThread; currentThread.SetWaitSleepJoinState(); - try + uint result; + if (millisecondsTimeout == Timeout.Infinite) { - uint result; - if (millisecondsTimeout == -1) + // Infinite wait + while (true) { - // Infinite wait - while (true) + result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), Timeout.UnsignedInfinite, Interop.BOOL.TRUE); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) { - result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), uint.MaxValue, Interop.BOOL.TRUE); - if (result != Interop.Kernel32.WAIT_IO_COMPLETION) - { - break; - } - // Check if this was our interrupt APC - CheckForPendingInterrupt(); + break; } + // Check if this was our interrupt APC + CheckForPendingInterrupt(); } - else + } + else + { + long startTime = Environment.TickCount64; + while (true) { - long startTime = Environment.TickCount64; - while (true) + result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), (uint)millisecondsTimeout, Interop.BOOL.TRUE); + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) { - result = Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), (uint)millisecondsTimeout, Interop.BOOL.TRUE); - if (result != Interop.Kernel32.WAIT_IO_COMPLETION) - { - break; - } - // Check if this was our interrupt APC - CheckForPendingInterrupt(); - // Handle APC completion by adjusting timeout and retrying - long currentTime = Environment.TickCount64; - long elapsed = currentTime - startTime; - if (elapsed >= millisecondsTimeout) - { - result = Interop.Kernel32.WAIT_TIMEOUT; - break; - } - millisecondsTimeout -= (int)elapsed; - startTime = currentTime; + break; + } + // Check if this was our interrupt APC + CheckForPendingInterrupt(); + // Handle APC completion by adjusting timeout and retrying + long currentTime = Environment.TickCount64; + long elapsed = currentTime - startTime; + if (elapsed >= millisecondsTimeout) + { + result = Interop.Kernel32.WAIT_TIMEOUT; + break; } + millisecondsTimeout -= (int)elapsed; + startTime = currentTime; } - return result == (int)Interop.Kernel32.WAIT_OBJECT_0; - } - finally - { - currentThread.ClearWaitSleepJoinState(); } + currentThread.ClearWaitSleepJoinState(); + return result == (int)Interop.Kernel32.WAIT_OBJECT_0; } } finally @@ -545,6 +521,7 @@ internal static void CheckForPendingInterrupt() { if (RuntimeImports.RhCheckAndClearPendingInterrupt()) { + CurrentThread.ClearWaitSleepJoinState(); throw new ThreadInterruptedException(); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs index 9a9f8227ab8c57..755ea77ba1cbec 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.Windows.cs @@ -56,56 +56,46 @@ private static unsafe int WaitForMultipleObjectsIgnoringSyncContext(IntPtr* pHan } int result; - try - { -#if !MONO - // Check for thread interrupt when APC completion occurs - Thread.CheckForPendingInterrupt(); -#endif - while (true) - { + Thread.CheckForPendingInterrupt(); + + while (true) + { #if NATIVEAOT - if (reentrantWait) - { - Debug.Assert(!waitAll); - result = RuntimeImports.RhCompatibleReentrantWaitAny(true, millisecondsTimeout, numHandles, pHandles); - } - else - { - result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.TRUE); - } -#else + if (reentrantWait) + { + Debug.Assert(!waitAll); + result = RuntimeImports.RhCompatibleReentrantWaitAny(true, millisecondsTimeout, numHandles, pHandles); + } + else + { result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.TRUE); + } +#else + result = (int)Interop.Kernel32.WaitForMultipleObjectsEx((uint)numHandles, (IntPtr)pHandles, waitAll ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, (uint)millisecondsTimeout, Interop.BOOL.TRUE); #endif - if (result != Interop.Kernel32.WAIT_IO_COMPLETION) - break; + if (result != Interop.Kernel32.WAIT_IO_COMPLETION) + break; -#if !MONO - // Check for thread interrupt when APC completion occurs - Thread.CheckForPendingInterrupt(); -#endif + Thread.CheckForPendingInterrupt(); - // Handle APC completion by adjusting timeout and retrying - if (millisecondsTimeout != -1) + // Handle APC completion by adjusting timeout and retrying + if (millisecondsTimeout != Timeout.Infinite) + { + long currentTime = Environment.TickCount64; + long elapsed = currentTime - startTime; + if (elapsed >= millisecondsTimeout) { - long currentTime = Environment.TickCount64; - long elapsed = currentTime - startTime; - if (elapsed >= millisecondsTimeout) - { - result = Interop.Kernel32.WAIT_TIMEOUT; - break; - } - millisecondsTimeout -= (int)elapsed; - startTime = currentTime; + result = Interop.Kernel32.WAIT_TIMEOUT; + break; } + millisecondsTimeout -= (int)elapsed; + startTime = currentTime; } } - finally - { - currentThread.ClearWaitSleepJoinState(); - } + + currentThread.ClearWaitSleepJoinState(); if (result == Interop.Kernel32.WAIT_FAILED) { @@ -150,10 +140,7 @@ private static int SignalAndWaitCore(IntPtr handleToSignal, IntPtr handleToWaitO startTime = Environment.TickCount64; } -#if !MONO - // Check for thread interrupt when APC completion occurs Thread.CheckForPendingInterrupt(); -#endif // Signal the object and wait for the first time int ret = (int)Interop.Kernel32.SignalObjectAndWait(handleToSignal, handleToWaitOn, (uint)millisecondsTimeout, Interop.BOOL.TRUE); @@ -161,10 +148,7 @@ private static int SignalAndWaitCore(IntPtr handleToSignal, IntPtr handleToWaitO // Handle APC completion by retrying with WaitForSingleObjectEx (without signaling again) while (ret == Interop.Kernel32.WAIT_IO_COMPLETION) { -#if !MONO - // Check for thread interrupt when APC completion occurs Thread.CheckForPendingInterrupt(); -#endif if (millisecondsTimeout != -1) { diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs index 5715dd23923f6b..c0c804db8b9998 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/Thread.Mono.cs @@ -364,5 +364,11 @@ internal bool HasExternalEventLoop external_eventloop = value; } } + +#if TARGET_WINDOWS + internal static void CheckForPendingInterrupt() + { + } +#endif } } From 49bc103062700775c51e3c1ce5ed85c22a92a0a4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 26 Aug 2025 16:15:45 -0700 Subject: [PATCH 09/10] Fix the "unstarted" interrupt case --- src/coreclr/nativeaot/Runtime/thread.cpp | 4 ++++ .../src/System/Threading/Thread.NativeAot.Windows.cs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/thread.cpp b/src/coreclr/nativeaot/Runtime/thread.cpp index e01d5c94077516..47c99ea2a93d0c 100644 --- a/src/coreclr/nativeaot/Runtime/thread.cpp +++ b/src/coreclr/nativeaot/Runtime/thread.cpp @@ -1346,6 +1346,10 @@ FCIMPLEND static VOID CALLBACK InterruptApcCallback(ULONG_PTR /* parameter */) { // Set the interrupt flag on the current thread + // If we were queued on a thread that wasn't started, + // we may be the first code that runs on this thread. + // Ensure we're attached to the thread store. + ThreadStore::AttachCurrentThread(); Thread* pCurrentThread = ThreadStore::RawGetCurrentThread(); if (pCurrentThread != nullptr) { diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index bf5c25c781603d..37652e3030b642 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -503,13 +503,13 @@ public void Interrupt() } // Thread.Interrupt for thread that has not been started yet should queue a pending interrupt - if (GetThreadStateBit(ThreadState.Unstarted)) + // for when we actually create the thread. + if (_osHandle?.IsInvalid ?? true) { SetThreadStateBit(Interrupted); return; } - Debug.Assert(!_osHandle.IsInvalid); unsafe { Interop.Kernel32.QueueUserAPC(RuntimeImports.RhGetInterruptApcCallback(), _osHandle, 0); From 7c432e809046a9fc382428c4dfef59c19ca4ba17 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 27 Aug 2025 10:24:34 -0700 Subject: [PATCH 10/10] Remove null checks and only attach uninitialized threads --- src/coreclr/nativeaot/Runtime/thread.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/thread.cpp b/src/coreclr/nativeaot/Runtime/thread.cpp index 47c99ea2a93d0c..7a1391f75b7b11 100644 --- a/src/coreclr/nativeaot/Runtime/thread.cpp +++ b/src/coreclr/nativeaot/Runtime/thread.cpp @@ -1345,16 +1345,16 @@ FCIMPLEND // This callback sets the interrupt flag on the current thread static VOID CALLBACK InterruptApcCallback(ULONG_PTR /* parameter */) { - // Set the interrupt flag on the current thread - // If we were queued on a thread that wasn't started, - // we may be the first code that runs on this thread. - // Ensure we're attached to the thread store. - ThreadStore::AttachCurrentThread(); Thread* pCurrentThread = ThreadStore::RawGetCurrentThread(); - if (pCurrentThread != nullptr) + if (!pCurrentThread->IsInitialized()) { - pCurrentThread->SetInterrupted(true); + // If the thread was interrupted before it was started + // the thread won't have been initialized. + // Attach the thread here if it's the first time we're seeing it. + ThreadStore::AttachCurrentThread(); } + + pCurrentThread->SetInterrupted(true); } // Function to get the address of the interrupt APC callback @@ -1367,7 +1367,7 @@ FCIMPLEND FCIMPL0(FC_BOOL_RET, RhCheckAndClearPendingInterrupt) { Thread* pCurrentThread = ThreadStore::RawGetCurrentThread(); - if (pCurrentThread != nullptr && pCurrentThread->CheckInterrupted()) + if (pCurrentThread->CheckInterrupted()) { pCurrentThread->SetInterrupted(false); FC_RETURN_BOOL(true);