Home > other >  Status is canceled in task without initialization with token and with ThrowIfCancellationRequested c
Status is canceled in task without initialization with token and with ThrowIfCancellationRequested c

Time:04-02

I thought that the canceled status of a task is set if (source):

The task acknowledged cancellation by throwing an OperationCanceledException with its own CancellationToken while the token was in signaled state, or the task's CancellationToken was already signaled before the task started executing.

But if you call ThrowIfCancellationRequested in the lambda in the task without initialization with token, then the status changes to canceled. Is this a bug or please explain what's going on?

Tested on C# for .NET 5 on VS 2019 and .NET 6 on VS 2022.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(500);

            void MyMethod()
            {
                while (true)
                {
                    Thread.Sleep(100);
                    cancellationTokenSource.Token.ThrowIfCancellationRequested();
                }
            }

            Task t1 = new Task(MyMethod);
            t1.Start();

            Task<int> t2 = new Task<int>(() => { MyMethod(); return 1; });
            t2.Start();

            Task t3 = Task.Factory.StartNew(MyMethod);

            Task t4 = Task.CompletedTask.ContinueWith((t) => { MyMethod(); });

            Task t5 = Task.Run(MyMethod);

            Task t6 = Task.Run(() =>
           {
               while (true)
               {
                   Thread.Sleep(100);
                   cancellationTokenSource.Token.ThrowIfCancellationRequested();
               }
           });

            try
            {
                Task.WaitAll(t1, t2, t3, t4, t5, t6);
            }
            catch
            {

            }

            Console.WriteLine($"Status t1 from new Task:                            {t1.Status}");  //Faulted
            Console.WriteLine($"Status t2 from new Task<:                           {t2.Status}");  //Faulted
            Console.WriteLine($"Status t3 from Task.Factory.StartNew:               {t3.Status}");  //Faulted
            Console.WriteLine($"Status t4 from Task.CompletedTask.ContinueWith:     {t4.Status}");  //Faulted
            Console.WriteLine("\n");
            Console.WriteLine($"Status t5 from Task.Run:                            {t5.Status}");  //Faulted
            Console.WriteLine($"Status t6 from Task.Run with lambda:                {t6.Status}");  //Canceled
        }
    }
}

Faulted instead of Canceled.

Online demo.

Output:

Status t1 from new Task:                        Faulted
Status t2 from new Task<int>:                   Faulted
Status t3 from Task.Factory.StartNew:           Faulted
Status t4 from Task.CompletedTask.ContinueWith: Faulted
Status t5 from Task.Run:                        Faulted
Status t6 from Task.Run with lambda:            Canceled

CodePudding user response:

It might be a bug in the C# compiler. Minimal reproduction:

using System;
using System.Threading.Tasks;

static class Program
{
    static async Task Main()
    {
        {
            var task = Task.Run(() => // Resolved to Task.Run(Action)
            {
                bool condition = true;
                if (condition) throw new OperationCanceledException();
            });
            try { await task; } catch { }
            Console.WriteLine($"if (condition): {task.Status}");
        }
        {
            var task = Task.Run(() => // Resolved to Task.Run(Func<Task>)
            {
                if (true) throw new OperationCanceledException();
            });
            try { await task; } catch { }
            Console.WriteLine($"if (true): {task.Status}");
        }
    }
}

Output:

if (condition): Faulted
if (true): Canceled

Online demo.

The Task.Run has many overloads, including one with an Action parameter, and one with a Func<Task> parameter. It seems that throwing unconditionally inside a while (true) or if (true) or for (; ; ) block in the lambda, triggers the asynchronous resolution.

The reason that the Task.Run(Func<Task>) overload results in a Canceled instead of Faulted task, is because the Task.Run(Func<Task>) is essentially a shortcut for this:

Task.Factory.StartNew<Task>(function,
    CancellationToken.None,
    TaskCreationOptions.DenyChildAttach,
    TaskScheduler.Default).Unwrap();

When the function is an async method, it always succeeds at producing a Task. This task is passed to the Unwrap, which propagates its status as is (the CancellationToken is not passed to the Unwrap). Also async methods by design transition to a canceled state when they encounter an OperationCanceledException. The combination of these two factors results in the observed inconsistent behavior. This behavior might be an oversight and not intentional. But since the Task.Run is already 10 years in production, it's probably too late to change it now.

  • Related