Skip to content

Counterintuitive behaviour of linked CancellationTokenSource #114264

@AlexMinchanka

Description

@AlexMinchanka

Description:
I faced with an issue with the CancellationTokenSource.CreateLinkedTokenSource . In some cases, when a parent CancellationTokenSource is canceled, the linked token does not always reflect that cancellation. This issue occurs intermittently, where the linked token remains uncanceled even after the parent token has been canceled.

Reproduction Steps:

  1. Use either a Parallel.For or standard for loop to run multiple asynchronous tasks concurrently.

  2. Inside each task, create a CancellationTokenSource with a timeout (e.g., 100ms).

  3. Register a linked token using CancellationTokenSource.CreateLinkedTokenSource.

  4. Introduce a slight delay (Task.Delay) before checking the cancellation status of both the parent and linked tokens.

Observe that, in some cases, the linked token does not reflect the cancellation state of the parent token, even though the parent token has been canceled.

namespace MyApp
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            var task = new Task[1000];

            // Use Parallel.For or a standard for loop
            // Option 1: Using Parallel.For
            // Parallel.For(0, task.Length, (i) =>
            // {
            //     task[i] = DoAsync();
            // });

            // Option 2: Using standard for loop
            for (int i = 0; i < task.Length; i++)
            {
                task[i] = DoAsync();
            }

            await Task.WhenAll(task);

            Console.WriteLine("Finish");
        }

        static async Task DoAsync()
        {
            var parentCts = new CancellationTokenSource(100);
            var parentToken = parentCts.Token;
            
            await Task.Delay(98);
            
            var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(parentCts.Token);
            var linkedToken = linkedCts.Token;

            
            var t1 = parentToken.IsCancellationRequested;
            var t2 = linkedToken.IsCancellationRequested;

            if (t1 != t2)
            {
                Console.WriteLine($"parent: {t1}, linked: {t2}");
            }
        }
    }
}

Output:

parent: True, linked: False
parent: True, linked: False
parent: True, linked: False
parent: True, linked: False
parent: True, linked: False
Finish

Expected Behavior:

  • After the parent token (parentToken) is canceled (after 100ms), the linked token (linkedToken) should also be canceled.

  • Both parentToken.IsCancellationRequested and linkedToken.IsCancellationRequested should return the same result, indicating both tokens were canceled at the same time.

  • The cancellation status should be consistent across all tasks, whether using Parallel.For or a standard for loop.

Actual Behavior:

  • Inconsistent cancellation status between the parent and linked tokens, especially in asynchronous tasks.

  • Some tasks report that the linked token is not canceled, even though the parent token was canceled.

Version Info:

  • .NET 9

  • Console application

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions