I thought that the canceled status of a task is set if (source):
The task acknowledged cancellation by throwing an
OperationCanceledException
with its ownCancellationToken
while the token was in signaled state, or the task'sCancellationToken
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
.
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
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.