Feature request checklist
Problem Statement
TUnit runs tests in parallel by default, but the documentation does not explain that CPU-bound Task.Run work in one test can starve async continuations in another test.
This can cause timed waits such as Task.WaitAsync(TimeSpan) to expire even though the awaited operation would succeed when run alone.
/// <summary>
/// Reproduces TUnit's interaction with CPU-bound Task.Run work and async test continuations.
/// Run this class with default parallel execution. The TCS test can fail because the CPU burner
/// prevents the task that signals the TCS from getting scheduled before the timed wait expires.
/// Adding [NotInParallel] to the TCS test or this class makes the test pass consistently.
/// </summary>
public sealed class TUnitThreadPoolStarvationReproTests
{
[Test]
public async Task TaskCompletionSourceWait_WhenCpuBurnerRunsInParallel_CanTimeout()
{
var started = 0;
var bothStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var waitingTask = Task.Run(async () =>
{
if (Interlocked.Increment(ref started) == 2)
{
bothStarted.TrySetResult();
}
await bothStarted.Task.WaitAsync(TimeSpan.FromSeconds(1));
});
// Delay scheduling the second task while the other test saturates the thread pool. Under
// TUnit's default parallel execution, it can miss the one-second rendezvous timeout.
await Task.Yield();
await Task.Delay(100);
await Task.Yield();
var signalingTask = Task.Run(async () =>
{
if (Interlocked.Increment(ref started) == 2)
{
bothStarted.TrySetResult();
}
await bothStarted.Task.WaitAsync(TimeSpan.FromSeconds(1));
});
await Task.WhenAll(waitingTask, signalingTask);
}
[Test]
public async Task Chaos_CPU_Burner()
{
// Saturate the available CPU with thread-pool work for longer than the TCS wait.
var tasks = Enumerable.Range(0, Environment.ProcessorCount * 2)
.Select(_ => Task.Run(() =>
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
while (stopwatch.ElapsedMilliseconds < 2000)
{
double _ = Math.Sqrt(12345.6789);
}
}));
await Task.WhenAll(tasks);
}
}
This was particularly difficult to diagnose because I assumed it to be an application bug, but was ultimately caused by unexpected TUnit test-scheduler/thread-pool starvation behavior.
Proposed Solution
Add a warning to the parallelism documentation explaining this interaction with the .NET thread pool.
Alternatives Considered
No response
Feature Category
Documentation
How important is this feature to you?
Important - significantly impacts my workflow
Additional Context
No response
Contribution
Feature request checklist
Problem Statement
TUnit runs tests in parallel by default, but the documentation does not explain that CPU-bound
Task.Runwork in one test can starve async continuations in another test.This can cause timed waits such as
Task.WaitAsync(TimeSpan)to expire even though the awaited operation would succeed when run alone.This was particularly difficult to diagnose because I assumed it to be an application bug, but was ultimately caused by unexpected TUnit test-scheduler/thread-pool starvation behavior.
Proposed Solution
Add a warning to the parallelism documentation explaining this interaction with the .NET thread pool.
Alternatives Considered
No response
Feature Category
Documentation
How important is this feature to you?
Important - significantly impacts my workflow
Additional Context
No response
Contribution