Check for pending IO in the portable thread pool's worker threads (#82245)
authorKoundinya Veluri <kouvel@users.noreply.github.com>
Wed, 1 Mar 2023 17:53:50 +0000 (09:53 -0800)
committerGitHub <noreply@github.com>
Wed, 1 Mar 2023 17:53:50 +0000 (09:53 -0800)
* Check for pending IO in the portable thread pool's worker threads

- When Resource Monitor is attached, some async IO operations are bound to the thread that issued it even though the IO handle is bound to an IOCP. If the thread exits, the async IO operation is aborted. This can lead to hangs or unexpected exceptions.
- Added a check that was missing in the portable thread pool implementation to prevent exiting a worker thread when it has pending IO

Fixes https://github.com/dotnet/runtime/issues/82207

src/libraries/Common/src/Interop/Windows/Kernel32/Interop.Threading.cs
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.Unix.cs [moved from src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.CpuUtilizationReader.Unix.cs with 80% similarity]
src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.Windows.cs [moved from src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.CpuUtilizationReader.Windows.cs with 75% similarity]
src/libraries/System.Private.CoreLib/src/System/Threading/PortableThreadPool.WorkerThread.cs

index f64a23f..9a53b48 100644 (file)
@@ -70,5 +70,9 @@ internal static partial class Interop
         [LibraryImport(Libraries.Kernel32)]
         [return:MarshalAs(UnmanagedType.Bool)]
         internal static partial bool SetThreadPriority(SafeWaitHandle hThread, int nPriority);
+
+        [LibraryImport(Libraries.Kernel32, SetLastError = true)]
+        [return: MarshalAs(UnmanagedType.Bool)]
+        internal static partial bool GetThreadIOPendingFlag(nint hThread, out BOOL lpIOIsPending);
     }
 }
index bbef0e9..b200e12 100644 (file)
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.SystemTimeToFileTime.cs">
       <Link>Common\Interop\Windows\Kernel32\Interop.SystemTimeToFileTime.cs</Link>
     </Compile>
+    <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.Threading.cs">
+      <Link>Common\Interop\Windows\Kernel32\Interop.Threading.cs</Link>
+    </Compile>
     <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.TimeZone.cs">
       <Link>Common\Interop\Windows\Kernel32\Interop.TimeZone.cs</Link>
     </Compile>
     <Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.WaitThread.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.WorkerThread.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.WorkerTracking.cs" />
-    <Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.CpuUtilizationReader.Unix.cs" Condition="'$(TargetsUnix)' == 'true' or '$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true'" />
-    <Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.CpuUtilizationReader.Windows.cs" Condition="'$(TargetsWindows)' == 'true'" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.Unix.cs" Condition="'$(TargetsUnix)' == 'true' or '$(TargetsBrowser)' == 'true' or '$(TargetsWasi)' == 'true'" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Threading\PortableThreadPool.Windows.cs" Condition="'$(TargetsWindows)' == 'true'" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Threading\LowLevelLifoSemaphore.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Threading\LowLevelLifoSemaphore.Windows.cs" Condition="'$(TargetsWindows)' == 'true'" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Threading\PreAllocatedOverlapped.cs" Condition="('$(TargetsBrowser)' != 'true' and '$(TargetsWasi)' != 'true') or '$(FeatureWasmThreads)' == 'true'" />
   </ItemGroup>
   <ItemGroup Condition="'$(FeatureCoreCLR)' != 'true' and '$(TargetsWindows)' == 'true'">
     <Compile Include="$(MSBuildThisFileDirectory)System\Threading\WaitHandle.Windows.cs" />
-    <Compile Include="$(CommonPath)\Interop\Windows\Kernel32\Interop.Threading.cs">
-      <Link>Interop\Windows\Kernel32\Interop.Threading.cs</Link>
-    </Compile>
   </ItemGroup>
   <ItemGroup>
     <Compile Include="$(MSBuildThisFileDirectory)System\IParsable.cs" />
@@ -5,6 +5,11 @@ namespace System.Threading
 {
     internal sealed partial class PortableThreadPool
     {
+        private static partial class WorkerThread
+        {
+            private static bool IsIOPending => false;
+        }
+
         private struct CpuUtilizationReader
         {
             private Interop.Sys.ProcessCpuInformation _cpuInfo;
@@ -8,6 +8,20 @@ namespace System.Threading
 {
     internal sealed partial class PortableThreadPool
     {
+        private static partial class WorkerThread
+        {
+            private static bool IsIOPending
+            {
+                get
+                {
+                    bool success =
+                        Interop.Kernel32.GetThreadIOPendingFlag(Interop.Kernel32.GetCurrentThread(), out Interop.BOOL isIOPending);
+                    Debug.Assert(success);
+                    return !success || isIOPending != Interop.BOOL.FALSE;
+                }
+            }
+        }
+
         private struct CpuUtilizationReader
         {
             public long _idleTime;
index 0e0323e..96578b9 100644 (file)
@@ -10,7 +10,7 @@ namespace System.Threading
         /// <summary>
         /// The worker thread infastructure for the CLR thread pool.
         /// </summary>
-        private static class WorkerThread
+        private static partial class WorkerThread
         {
             private const int SemaphoreSpinCountDefaultBaseline = 70;
 #if !TARGET_ARM64 && !TARGET_ARM && !TARGET_LOONGARCH64
@@ -115,6 +115,12 @@ namespace System.Threading
                         }
                     }
 
+                    // The thread cannot exit if it has IO pending, otherwise the IO may be canceled
+                    if (IsIOPending)
+                    {
+                        continue;
+                    }
+
                     threadAdjustmentLock.Acquire();
                     try
                     {