diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj index 2c96332adf7d00..7363ef34e2730d 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj @@ -284,6 +284,12 @@ Interop\Windows\Kernel32\Interop.DynamicLoad.cs + + Interop\Windows\Kernel32\Interop.QueueUserAPC.cs + + + Interop\Windows\Kernel32\Interop.Threading.cs + 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 12a2273eddf605..d8089758b317a7 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 @@ -20,10 +20,15 @@ public sealed partial class Thread [ThreadStatic] private static ComState t_comState; + [ThreadStatic] + private static bool t_interruptRequested; + private SafeWaitHandle _osHandle; private ApartmentState _initialApartmentState = ApartmentState.Unknown; + private volatile bool _pendingInterrupt; + partial void PlatformSpecificInitialize(); // Platform-specific initialization of foreign threads, i.e. threads not created by Thread.Start @@ -162,7 +167,40 @@ private bool JoinInternal(int millisecondsTimeout) } else { - result = WaitHandle.WaitOneCore(waitHandle.DangerousGetHandle(), millisecondsTimeout, useTrivialWaits: false); + Thread? currentThread = t_currentThread; + + // Check for pending interrupt from before thread started + if (currentThread is not null && currentThread._pendingInterrupt) + { + currentThread._pendingInterrupt = false; + throw new ThreadInterruptedException(); + } + + if (currentThread is not null) + { + currentThread.SetWaitSleepJoinState(); + } + + try + { + // Use alertable wait so we can be interrupted by APC + result = (int)Interop.Kernel32.WaitForSingleObjectEx(waitHandle.DangerousGetHandle(), + (uint)millisecondsTimeout, Interop.BOOL.TRUE); + + // Check if we were interrupted by an APC + if (result == Interop.Kernel32.WAIT_IO_COMPLETION) + { + CheckForInterrupt(); + return false; // Interrupted, so join did not complete + } + } + finally + { + if (currentThread is not null) + { + currentThread.ClearWaitSleepJoinState(); + } + } } return result == (int)Interop.Kernel32.WAIT_OBJECT_0; @@ -226,6 +264,21 @@ private static uint ThreadEntryPoint(IntPtr parameter) return 0; } + private static void CheckPendingInterrupt() + { + Thread? currentThread = t_currentThread; + if (currentThread is not null && currentThread._pendingInterrupt) + { + currentThread._pendingInterrupt = false; + throw new ThreadInterruptedException(); + } + } + + private static void CheckForPendingInterrupt() + { + CheckPendingInterrupt(); + } + public ApartmentState GetApartmentState() { if (this != CurrentThread) @@ -386,7 +439,51 @@ internal static Thread EnsureThreadPoolThreadInitialized() return InitializeExistingThreadPoolThread(); } - public void Interrupt() { throw new PlatformNotSupportedException(); } + [UnmanagedCallersOnly] + private static void InterruptApcCallback(nint parameter) + { + // This is the native APC callback that sets the interrupt flag + // It runs in native code to avoid managed reentrancy issues + t_interruptRequested = true; + } + + private static void CheckForInterrupt() + { + if (t_interruptRequested) + { + t_interruptRequested = false; + throw new ThreadInterruptedException(); + } + } + + public void Interrupt() + { + using (_lock.EnterScope()) + { + // If thread is dead, do nothing + if (GetThreadStateBit(ThreadState.Stopped)) + return; + + // If thread hasn't started yet, set pending interrupt flag + if (GetThreadStateBit(ThreadState.Unstarted)) + { + _pendingInterrupt = true; + return; + } + + // Queue APC to interrupt the thread + SafeWaitHandle osHandle = _osHandle; + if (osHandle is not null && !osHandle.IsInvalid && !osHandle.IsClosed) + { + nint callbackPtr; + unsafe + { + callbackPtr = (nint)(delegate* unmanaged)&InterruptApcCallback; + } + Interop.Kernel32.QueueUserAPC(callbackPtr, osHandle.DangerousGetHandle(), IntPtr.Zero); + } + } + } 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..a101c27fb23b7d 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 @@ -46,6 +46,9 @@ public sealed partial class Thread private static int s_foregroundRunningCount; + // Platform-specific method to check for pending interrupts when thread starts + partial void CheckForPendingInterrupt(); + private Thread() { _managedThreadId = System.Threading.ManagedThreadId.GetCurrentThreadId(); @@ -450,6 +453,9 @@ private static void StartThread(IntPtr parameter) IncrementRunningForeground(); } + // Check for any pending interrupt that was queued before the thread started + thread.CheckForPendingInterrupt(); + try { StartHelper? startHelper = thread._startHelper; diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueueUserAPC.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueueUserAPC.cs new file mode 100644 index 00000000000000..fa36bd32b4922b --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.QueueUserAPC.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + internal delegate void PAPCFUNC(nint dwParam); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool QueueUserAPC(nint pfnAPC, nint hThread, nint dwData); + } +} \ No newline at end of file diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SleepEx.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SleepEx.cs new file mode 100644 index 00000000000000..bab2a3bff71256 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.SleepEx.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [LibraryImport(Libraries.Kernel32)] + internal static partial uint SleepEx(uint dwMilliseconds, BOOL bAlertable); + } +} \ No newline at end of file diff --git a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs index 908d690e7e207d..eaa31ed026a407 100644 --- a/src/libraries/System.Threading.Thread/tests/ThreadTests.cs +++ b/src/libraries/System.Threading.Thread/tests/ThreadTests.cs @@ -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 34aba004040a9d..e44d860a6b23de 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; }