From 652b607b7c522b67ce45010d0e1ac3a19b1f1eda Mon Sep 17 00:00:00 2001 From: Eduardo Velarde <32459232+eduardo-vp@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:53:10 -0700 Subject: [PATCH 1/5] Stop counting work items from ThreadPoolTypedWorkItemQueue for ThreadPool.CompletedWorkItemCount (#106854) * Stop counting work items from ThreadPoolTypedWorkItemQueue as completed work items * Fix CompletedWorkItemCount * Update src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs Co-authored-by: Koundinya Veluri * Run CompletedWorkItemCountTest on Windows only --------- Co-authored-by: Eduardo Manuel Velarde Polar Co-authored-by: Koundinya Veluri --- .../System/Threading/ThreadPoolWorkQueue.cs | 6 ++++- .../tests/ThreadPoolTests.cs | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs index 696c2960b05b5b..a66bc3761df46e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @@ -1247,7 +1247,11 @@ void IThreadPoolWorkItem.Execute() currentThread.ResetThreadPoolThread(); } - ThreadInt64PersistentCounter.Add(tl.threadLocalCompletionCountObject!, completedCount); + // Discount a work item here to avoid counting most of the queue processing work items + if (completedCount > 1) + { + ThreadInt64PersistentCounter.Add(tl.threadLocalCompletionCountObject!, completedCount - 1); + } } } diff --git a/src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs b/src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs index 59764268029fac..3e581520a29511 100644 --- a/src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs +++ b/src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs @@ -1265,6 +1265,33 @@ public static void PrioritizationExperimentConfigVarTest() }).Dispose(); } + + [ConditionalFact(nameof(IsThreadingAndRemoteExecutorSupported))] + [PlatformSpecific(TestPlatforms.Windows)] + public static unsafe void ThreadPoolCompletedWorkItemCountTest() + { + // Run in a separate process to test in a clean thread pool environment such that we don't count external work items + RemoteExecutor.Invoke(() => + { + using var manualResetEvent = new ManualResetEventSlim(false); + + var overlapped = new Overlapped(); + NativeOverlapped* nativeOverlapped = overlapped.Pack((errorCode, numBytes, innerNativeOverlapped) => + { + Overlapped.Free(innerNativeOverlapped); + manualResetEvent.Set(); + }, null); + + ThreadPool.UnsafeQueueNativeOverlapped(nativeOverlapped); + manualResetEvent.Wait(); + + // Allow work item(s) to be marked as completed during this time, should be only one + ThreadTestHelpers.WaitForCondition(() => ThreadPool.CompletedWorkItemCount == 1); + Thread.Sleep(50); + Assert.Equal(1, ThreadPool.CompletedWorkItemCount); + }).Dispose(); + } + public static bool IsThreadingAndRemoteExecutorSupported => PlatformDetection.IsThreadingSupported && RemoteExecutor.IsSupported; From a544893f68fd291d68a37b330ac49131f87cef56 Mon Sep 17 00:00:00 2001 From: Koundinya Veluri Date: Tue, 18 Feb 2025 10:07:17 -0800 Subject: [PATCH 2/5] Make counting of IO completion work items more precise on Windows - Follow-up to https://github.com/dotnet/runtime/pull/106854. Issue: https://github.com/dotnet/runtime/issues/104284. - Before the change, the modified test case often yields 5 or 6 completed work items, due to queue-processing work items that happen to not process any user work items. After the change, it always yields 4. - Looks like it doesn't hurt to have more-precise counting, and there was a request to backport a fix to .NET 8, where it's more necessary to fix the issue --- .../Threading/ThreadInt64PersistentCounter.cs | 23 ++++++++++++-- .../src/System/Threading/ThreadPool.Unix.cs | 15 ++++++++- .../System/Threading/ThreadPool.Windows.cs | 18 ++++++++++- .../System/Threading/ThreadPoolWorkQueue.cs | 7 ++++- .../tests/ThreadPoolTests.cs | 31 ++++++++++++------- 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs index 0f7fbc06a9a8eb..bba3ee1dd60752 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs @@ -31,6 +31,13 @@ public static void Increment(object threadLocalCountObject) Unsafe.As(threadLocalCountObject).Increment(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Decrement(object threadLocalCountObject) + { + Debug.Assert(threadLocalCountObject is ThreadLocalNode); + Unsafe.As(threadLocalCountObject).Decrement(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Add(object threadLocalCountObject, uint count) { @@ -134,6 +141,18 @@ public void Increment() OnAddOverflow(1); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Decrement() + { + if (_count != 0) + { + _count--; + return; + } + + OnAddOverflow(-1); + } + public void Add(uint count) { Debug.Assert(count != 0); @@ -149,7 +168,7 @@ public void Add(uint count) } [MethodImpl(MethodImplOptions.NoInlining)] - private void OnAddOverflow(uint count) + private void OnAddOverflow(long count) { Debug.Assert(count != 0); @@ -161,7 +180,7 @@ private void OnAddOverflow(uint count) counter._lock.Acquire(); try { - counter._overflowCount += (long)_count + count; + counter._overflowCount += _count + count; _count = 0; } finally diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Unix.cs index 3c5888a6f80dbd..3a0d0ebde374aa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Unix.cs @@ -131,7 +131,20 @@ public static long CompletedWorkItemCount { get { - return PortableThreadPool.ThreadPoolInstance.CompletedWorkItemCount; + long count = PortableThreadPool.ThreadPoolInstance.CompletedWorkItemCount; + + // Ensure that the returned value is monotonically increasing + long lastCount = s_lastCompletedWorkItemCount; + if (count > lastCount) + { + s_lastCompletedWorkItemCount = count; + } + else + { + count = lastCount; + } + + return count; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs index 0da875498afc18..b0108b0ea5dc8a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs @@ -201,7 +201,23 @@ public static long CompletedWorkItemCount { get { - return ThreadPool.UseWindowsThreadPool ? WindowsThreadPool.CompletedWorkItemCount : PortableThreadPool.ThreadPoolInstance.CompletedWorkItemCount; + long count = + UseWindowsThreadPool + ? WindowsThreadPool.CompletedWorkItemCount + : PortableThreadPool.ThreadPoolInstance.CompletedWorkItemCount; + + // Ensure that the returned value is monotonically increasing + long lastCount = s_lastCompletedWorkItemCount; + if (count > lastCount) + { + s_lastCompletedWorkItemCount = count; + } + else + { + count = lastCount; + } + + return count; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs index a66bc3761df46e..3e814f872ef7a5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @@ -1204,6 +1204,9 @@ void IThreadPoolWorkItem.Execute() Interlocked.MemoryBarrier(); if (!_workItems.TryDequeue(out T workItem)) { + // Discount a work item here to avoid counting this queue processing work item + ThreadInt64PersistentCounter.Decrement( + ThreadPoolWorkQueueThreadLocals.threadLocals!.threadLocalCompletionCountObject!); return; } @@ -1247,7 +1250,7 @@ void IThreadPoolWorkItem.Execute() currentThread.ResetThreadPoolThread(); } - // Discount a work item here to avoid counting most of the queue processing work items + // Discount a work item here to avoid counting this queue processing work item if (completedCount > 1) { ThreadInt64PersistentCounter.Add(tl.threadLocalCompletionCountObject!, completedCount - 1); @@ -1454,6 +1457,8 @@ public static partial class ThreadPool internal static readonly ThreadPoolWorkQueue s_workQueue = new ThreadPoolWorkQueue(); + private static long s_lastCompletedWorkItemCount; + /// Shim used to invoke of the supplied . internal static readonly Action s_invokeAsyncStateMachineBox = static state => { diff --git a/src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs b/src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs index 3e581520a29511..2d305482d9e114 100644 --- a/src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs +++ b/src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs @@ -1273,22 +1273,31 @@ public static unsafe void ThreadPoolCompletedWorkItemCountTest() // Run in a separate process to test in a clean thread pool environment such that we don't count external work items RemoteExecutor.Invoke(() => { - using var manualResetEvent = new ManualResetEventSlim(false); + const int WorkItemCount = 4; - var overlapped = new Overlapped(); - NativeOverlapped* nativeOverlapped = overlapped.Pack((errorCode, numBytes, innerNativeOverlapped) => + int completedWorkItemCount = 0; + using var allWorkItemsCompleted = new AutoResetEvent(false); + + IOCompletionCallback callback = + (errorCode, numBytes, innerNativeOverlapped) => + { + Overlapped.Free(innerNativeOverlapped); + if (Interlocked.Increment(ref completedWorkItemCount) == WorkItemCount) + { + allWorkItemsCompleted.Set(); + } + }; + for (int i = 0; i < WorkItemCount; i++) { - Overlapped.Free(innerNativeOverlapped); - manualResetEvent.Set(); - }, null); + ThreadPool.UnsafeQueueNativeOverlapped(new Overlapped().Pack(callback, null)); + } - ThreadPool.UnsafeQueueNativeOverlapped(nativeOverlapped); - manualResetEvent.Wait(); + allWorkItemsCompleted.CheckedWait(); - // Allow work item(s) to be marked as completed during this time, should be only one - ThreadTestHelpers.WaitForCondition(() => ThreadPool.CompletedWorkItemCount == 1); + // Allow work items to be marked as completed during this time + ThreadTestHelpers.WaitForCondition(() => ThreadPool.CompletedWorkItemCount >= WorkItemCount); Thread.Sleep(50); - Assert.Equal(1, ThreadPool.CompletedWorkItemCount); + Assert.Equal(WorkItemCount, ThreadPool.CompletedWorkItemCount); }).Dispose(); } From 935f7955968590122c8a0a779611e075efe6f312 Mon Sep 17 00:00:00 2001 From: Koundinya Veluri Date: Thu, 20 Feb 2025 04:01:57 -0800 Subject: [PATCH 3/5] Allow unsafe code in test project --- .../tests/System.Threading.ThreadPool.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Threading.ThreadPool/tests/System.Threading.ThreadPool.Tests.csproj b/src/libraries/System.Threading.ThreadPool/tests/System.Threading.ThreadPool.Tests.csproj index 0cb21c9d38492b..a50fbed0ffb702 100644 --- a/src/libraries/System.Threading.ThreadPool/tests/System.Threading.ThreadPool.Tests.csproj +++ b/src/libraries/System.Threading.ThreadPool/tests/System.Threading.ThreadPool.Tests.csproj @@ -2,6 +2,7 @@ true $(NetCoreAppCurrent) + true true From d7aca72ee6ca3d3c160200b0e94f353ab35e14a4 Mon Sep 17 00:00:00 2001 From: Koundinya Veluri Date: Fri, 21 Feb 2025 10:31:25 -0800 Subject: [PATCH 4/5] Address feedback --- .../Threading/ThreadInt64PersistentCounter.cs | 12 ++++++++++++ .../src/System/Threading/ThreadPool.Unix.cs | 15 +-------------- .../src/System/Threading/ThreadPool.Windows.cs | 18 +----------------- .../System/Threading/ThreadPoolWorkQueue.cs | 2 -- 4 files changed, 14 insertions(+), 33 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs index bba3ee1dd60752..29cf2dce305657 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs @@ -15,6 +15,7 @@ internal sealed class ThreadInt64PersistentCounter private static List? t_nodeFinalizationHelpers; private long _overflowCount; + private long _lastReturnedCount; // dummy node serving as a start and end of the ring list private readonly ThreadLocalNode _nodes; @@ -83,6 +84,17 @@ public long Count count += node.Count; node = node._next; } + + // Ensure that the returned value is monotonically increasing + long lastReturnedCount = _lastReturnedCount; + if (count > lastReturnedCount) + { + _lastReturnedCount = count; + } + else + { + count = lastReturnedCount; + } } finally { diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Unix.cs index 3a0d0ebde374aa..3c5888a6f80dbd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Unix.cs @@ -131,20 +131,7 @@ public static long CompletedWorkItemCount { get { - long count = PortableThreadPool.ThreadPoolInstance.CompletedWorkItemCount; - - // Ensure that the returned value is monotonically increasing - long lastCount = s_lastCompletedWorkItemCount; - if (count > lastCount) - { - s_lastCompletedWorkItemCount = count; - } - else - { - count = lastCount; - } - - return count; + return PortableThreadPool.ThreadPoolInstance.CompletedWorkItemCount; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs index b0108b0ea5dc8a..0da875498afc18 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPool.Windows.cs @@ -201,23 +201,7 @@ public static long CompletedWorkItemCount { get { - long count = - UseWindowsThreadPool - ? WindowsThreadPool.CompletedWorkItemCount - : PortableThreadPool.ThreadPoolInstance.CompletedWorkItemCount; - - // Ensure that the returned value is monotonically increasing - long lastCount = s_lastCompletedWorkItemCount; - if (count > lastCount) - { - s_lastCompletedWorkItemCount = count; - } - else - { - count = lastCount; - } - - return count; + return ThreadPool.UseWindowsThreadPool ? WindowsThreadPool.CompletedWorkItemCount : PortableThreadPool.ThreadPoolInstance.CompletedWorkItemCount; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs index 3e814f872ef7a5..ebd1e58bae4fe1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs @@ -1457,8 +1457,6 @@ public static partial class ThreadPool internal static readonly ThreadPoolWorkQueue s_workQueue = new ThreadPoolWorkQueue(); - private static long s_lastCompletedWorkItemCount; - /// Shim used to invoke of the supplied . internal static readonly Action s_invokeAsyncStateMachineBox = static state => { From 6bf7d8bc495d06b99944c686e8710dde93163810 Mon Sep 17 00:00:00 2001 From: Koundinya Veluri Date: Fri, 28 Feb 2025 10:14:33 -0800 Subject: [PATCH 5/5] Fix test project to allow unsafe code --- .../System.Threading.ThreadPool.WindowsThreadPool.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Threading.ThreadPool/tests/WindowsThreadPool/System.Threading.ThreadPool.WindowsThreadPool.Tests.csproj b/src/libraries/System.Threading.ThreadPool/tests/WindowsThreadPool/System.Threading.ThreadPool.WindowsThreadPool.Tests.csproj index f39bea77d0fbcd..b4925716d96e2c 100644 --- a/src/libraries/System.Threading.ThreadPool/tests/WindowsThreadPool/System.Threading.ThreadPool.WindowsThreadPool.Tests.csproj +++ b/src/libraries/System.Threading.ThreadPool/tests/WindowsThreadPool/System.Threading.ThreadPool.WindowsThreadPool.Tests.csproj @@ -3,6 +3,7 @@ true $(NetCoreAppCurrent)-windows + true true