Skip to content

[Feature]: Document thread-pool starvation under parallel tests #6812

Description

@aheubusch

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

  • I'm willing to submit a pull request for this feature

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions