Skip to content

Commit 0ffc916

Browse files
kouveleduardo-vpEduardo Manuel Velarde Polar
authored
[8.0] Make counting of IO completion work items more precise on Windows (#112795)
* Stop counting work items from ThreadPoolTypedWorkItemQueue for ThreadPool.CompletedWorkItemCount (#106854) * Stop counting work items from ThreadPoolTypedWorkItemQueue as completed work items --------- Co-authored-by: Eduardo Manuel Velarde Polar <[email protected]> Co-authored-by: Koundinya Veluri <[email protected]> * Make counting of IO completion work items more precise on Windows - Follow-up to #106854. Issue: #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 --------- Co-authored-by: Eduardo Velarde <[email protected]> Co-authored-by: Eduardo Manuel Velarde Polar <[email protected]>
1 parent 5748afb commit 0ffc916

File tree

5 files changed

+79
-3
lines changed

5 files changed

+79
-3
lines changed

src/libraries/System.Private.CoreLib/src/System/Threading/ThreadInt64PersistentCounter.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ internal sealed class ThreadInt64PersistentCounter
1515
private static List<ThreadLocalNodeFinalizationHelper>? t_nodeFinalizationHelpers;
1616

1717
private long _overflowCount;
18+
private long _lastReturnedCount;
1819

1920
// dummy node serving as a start and end of the ring list
2021
private readonly ThreadLocalNode _nodes;
@@ -31,6 +32,13 @@ public static void Increment(object threadLocalCountObject)
3132
Unsafe.As<ThreadLocalNode>(threadLocalCountObject).Increment();
3233
}
3334

35+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
36+
public static void Decrement(object threadLocalCountObject)
37+
{
38+
Debug.Assert(threadLocalCountObject is ThreadLocalNode);
39+
Unsafe.As<ThreadLocalNode>(threadLocalCountObject).Decrement();
40+
}
41+
3442
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3543
public static void Add(object threadLocalCountObject, uint count)
3644
{
@@ -76,6 +84,17 @@ public long Count
7684
count += node.Count;
7785
node = node._next;
7886
}
87+
88+
// Ensure that the returned value is monotonically increasing
89+
long lastReturnedCount = _lastReturnedCount;
90+
if (count > lastReturnedCount)
91+
{
92+
_lastReturnedCount = count;
93+
}
94+
else
95+
{
96+
count = lastReturnedCount;
97+
}
7998
}
8099
finally
81100
{
@@ -134,6 +153,18 @@ public void Increment()
134153
OnAddOverflow(1);
135154
}
136155

156+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
157+
public void Decrement()
158+
{
159+
if (_count != 0)
160+
{
161+
_count--;
162+
return;
163+
}
164+
165+
OnAddOverflow(-1);
166+
}
167+
137168
public void Add(uint count)
138169
{
139170
Debug.Assert(count != 0);
@@ -149,7 +180,7 @@ public void Add(uint count)
149180
}
150181

151182
[MethodImpl(MethodImplOptions.NoInlining)]
152-
private void OnAddOverflow(uint count)
183+
private void OnAddOverflow(long count)
153184
{
154185
Debug.Assert(count != 0);
155186

@@ -161,7 +192,7 @@ private void OnAddOverflow(uint count)
161192
counter._lock.Acquire();
162193
try
163194
{
164-
counter._overflowCount += (long)_count + count;
195+
counter._overflowCount += _count + count;
165196
_count = 0;
166197
}
167198
finally

src/libraries/System.Private.CoreLib/src/System/Threading/ThreadPoolWorkQueue.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,9 @@ void IThreadPoolWorkItem.Execute()
12041204
Interlocked.MemoryBarrier();
12051205
if (!_workItems.TryDequeue(out T workItem))
12061206
{
1207+
// Discount a work item here to avoid counting this queue processing work item
1208+
ThreadInt64PersistentCounter.Decrement(
1209+
ThreadPoolWorkQueueThreadLocals.threadLocals!.threadLocalCompletionCountObject!);
12071210
return;
12081211
}
12091212

@@ -1247,7 +1250,11 @@ void IThreadPoolWorkItem.Execute()
12471250
currentThread.ResetThreadPoolThread();
12481251
}
12491252

1250-
ThreadInt64PersistentCounter.Add(tl.threadLocalCompletionCountObject!, completedCount);
1253+
// Discount a work item here to avoid counting this queue processing work item
1254+
if (completedCount > 1)
1255+
{
1256+
ThreadInt64PersistentCounter.Add(tl.threadLocalCompletionCountObject!, completedCount - 1);
1257+
}
12511258
}
12521259
}
12531260

src/libraries/System.Threading.ThreadPool/tests/System.Threading.ThreadPool.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<PropertyGroup>
33
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
44
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
5+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
56
<TestRuntime>true</TestRuntime>
67
</PropertyGroup>
78
<ItemGroup>

src/libraries/System.Threading.ThreadPool/tests/ThreadPoolTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,42 @@ public static void PrioritizationExperimentConfigVarTest()
12651265
}).Dispose();
12661266
}
12671267

1268+
1269+
[ConditionalFact(nameof(IsThreadingAndRemoteExecutorSupported))]
1270+
[PlatformSpecific(TestPlatforms.Windows)]
1271+
public static unsafe void ThreadPoolCompletedWorkItemCountTest()
1272+
{
1273+
// Run in a separate process to test in a clean thread pool environment such that we don't count external work items
1274+
RemoteExecutor.Invoke(() =>
1275+
{
1276+
const int WorkItemCount = 4;
1277+
1278+
int completedWorkItemCount = 0;
1279+
using var allWorkItemsCompleted = new AutoResetEvent(false);
1280+
1281+
IOCompletionCallback callback =
1282+
(errorCode, numBytes, innerNativeOverlapped) =>
1283+
{
1284+
Overlapped.Free(innerNativeOverlapped);
1285+
if (Interlocked.Increment(ref completedWorkItemCount) == WorkItemCount)
1286+
{
1287+
allWorkItemsCompleted.Set();
1288+
}
1289+
};
1290+
for (int i = 0; i < WorkItemCount; i++)
1291+
{
1292+
ThreadPool.UnsafeQueueNativeOverlapped(new Overlapped().Pack(callback, null));
1293+
}
1294+
1295+
allWorkItemsCompleted.CheckedWait();
1296+
1297+
// Allow work items to be marked as completed during this time
1298+
ThreadTestHelpers.WaitForCondition(() => ThreadPool.CompletedWorkItemCount >= WorkItemCount);
1299+
Thread.Sleep(50);
1300+
Assert.Equal(WorkItemCount, ThreadPool.CompletedWorkItemCount);
1301+
}).Dispose();
1302+
}
1303+
12681304
public static bool IsThreadingAndRemoteExecutorSupported =>
12691305
PlatformDetection.IsThreadingSupported && RemoteExecutor.IsSupported;
12701306

src/libraries/System.Threading.ThreadPool/tests/WindowsThreadPool/System.Threading.ThreadPool.WindowsThreadPool.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
44
<!-- This test project is Windows only as it uses the Windows Threadpool -->
55
<TargetFramework>$(NetCoreAppCurrent)-windows</TargetFramework>
6+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
67
<TestRuntime>true</TestRuntime>
78
</PropertyGroup>
89
<ItemGroup>

0 commit comments

Comments
 (0)