CancellationToken.ThrowIfCancellationRequested 之后的故障与取消任务状态 [英] Faulted vs Canceled task status after CancellationToken.ThrowIfCancellationRequested
问题描述
通常我不会发布带有答案的问题,但这次我想吸引一些人注意我认为可能是一个晦涩但常见的问题.它是由这个问题触发的,从那时起我查看了自己的旧代码,发现其中一些也受此影响.
下面的代码启动并等待两个任务,task1
和 task2
,它们几乎相同.task1
与 task2
的唯一区别在于它运行一个永无止境的循环.IMO,对于某些执行 CPU 密集型工作的现实场景,这两种情况都非常典型.
使用系统;使用 System.Threading;使用 System.Threading.Tasks;命名空间控制台应用程序{公开课计划{静态异步任务 TestAsync(){var ct = new CancellationTokenSource(millisecondsDelay: 1000);var token = ct.Token;//启动任务 1var task1 = Task.Run(() =>{for (var i = 0; ; i++){Thread.Sleep(i);//模拟工作项#itoken.ThrowIfCancellationRequested();}});//启动任务2var task2 = Task.Run(() =>{for (var i = 0; i <1000; i++){Thread.Sleep(i);//模拟工作项#itoken.ThrowIfCancellationRequested();}});//等待任务 1尝试{等待任务1;}捕获(异常前){Console.WriteLine(new { task = "task1", ex.Message, task1.Status });}//等待任务 2尝试{等待任务2;}捕获(异常前){Console.WriteLine(new { task = "task2", ex.Message, task2.Status });}}public static void Main(string[] args){TestAsync().Wait();Console.WriteLine("回车退出...");Console.ReadLine();}}}
小提琴在这里.输出:
<前>{ task = task1,Message = 操作被取消.,Status = Canceled }{ task = task2,Message = 操作被取消.,Status = Faulted }为什么task1
的状态是Cancelled
,而task2
的状态是Faulted
? 请注意,在这两种情况下,我都不将 token
作为第二个参数传递给 Task.Run
.
这里有两个问题.首先,将 CancellationToken
传递给 Task.Run
API 总是一个好主意,除了使其可用于任务的 lambda.这样做会将令牌与任务相关联,并且对于 token.ThrowIfCancellationRequested
触发的取消的正确传播至关重要.
然而,这并不能解释为什么 task1
的取消状态仍然被正确传播(task1.Status == TaskStatus.Canceled
),而对于task2
(task2.Status == TaskStatus.Faulted
).
现在,这可能是巧妙的 C# 类型推断逻辑违背开发人员意愿的极少数情况之一.此处和这里.综上所述,对于task1
,编译器推断出以下对Task.Run
的覆盖:
public static Task Run(Func函数)
而不是:
public static Task Run(Action action)
那是因为 task1
lambda 没有 for
循环之外的自然代码路径,所以它也可能是一个 Func
lambda,尽管它不是 async
并且它不返回任何内容.这是编译器比 Action
更喜欢的选项.然后,使用这样的 Task.Run
覆盖相当于:
var task1 = Task.Factory.StartNew(new Func(() =>;{for (var i = 0; ; i++){Thread.Sleep(i);//模拟工作项#itoken.ThrowIfCancellationRequested();}})).解包();
Task
类型的嵌套任务由 Task.Factory.StartNew
返回,它获取 解包到 Task
由 Unwrap()代码>.
Task.Run
很聪明足以在它接受Func
时自动进行这种解包.未包装的承诺样式任务正确地传播了其内部任务的取消状态,由 Func
lambda 作为 OperationCanceledException
异常抛出.task2
不会发生这种情况,它接受一个 Action
lambda 并且不创建任何内部任务.task2
的取消不会传播,因为 token
尚未通过 Task.Run
与 task2
关联.
最后,这可能是 task1
想要的行为(当然不是 task2
),但我们不想在幕后创建嵌套任务无论哪种情况.此外,通过在 for
循环之外引入条件 break
,task1
的这种行为很容易被破坏.
task1
的正确代码应该是这样的:
var task1 = Task.Run(new Action(() =>{for (var i = 0; ; i++){Thread.Sleep(i);//模拟工作项#itoken.ThrowIfCancellationRequested();}}), 令牌);
Usually I don't post a question with the answer, but this time I'd like to attract some attention to what I think might be an obscure yet common issue. It was triggered by this question, since then I reviewed my own old code and found some of it was affected by this, too.
The code below starts and awaits two tasks, task1
and task2
, which are almost identical. task1
is only different from task2
in that it runs a never-ending loop. IMO, both cases are quite typical for some real-life scenarios performing CPU-bound work.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
public class Program
{
static async Task TestAsync()
{
var ct = new CancellationTokenSource(millisecondsDelay: 1000);
var token = ct.Token;
// start task1
var task1 = Task.Run(() =>
{
for (var i = 0; ; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
});
// start task2
var task2 = Task.Run(() =>
{
for (var i = 0; i < 1000; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
});
// await task1
try
{
await task1;
}
catch (Exception ex)
{
Console.WriteLine(new { task = "task1", ex.Message, task1.Status });
}
// await task2
try
{
await task2;
}
catch (Exception ex)
{
Console.WriteLine(new { task = "task2", ex.Message, task2.Status });
}
}
public static void Main(string[] args)
{
TestAsync().Wait();
Console.WriteLine("Enter to exit...");
Console.ReadLine();
}
}
}
The fiddle is here. The output:
{ task = task1, Message = The operation was canceled., Status = Canceled } { task = task2, Message = The operation was canceled., Status = Faulted }
Why the status of task1
is Cancelled
, but the status of task2
is Faulted
? Note, in both cases I do not pass token
as the 2nd parameter to Task.Run
.
There are two problems here. First, it's always a good idea to pass CancellationToken
to the Task.Run
API, besides making it available to the task's lambda. Doing so associates the token with the task and is vital for the correct propagation of the cancellation triggered by token.ThrowIfCancellationRequested
.
This however doesn't explain why the cancellation status for task1
still gets propagated correctly (task1.Status == TaskStatus.Canceled
), while it doesn't for task2
(task2.Status == TaskStatus.Faulted
).
Now, this might be one of those very rare cases where the clever C# type inference logic can play against the developer's will. It's discussed in great details here and here. To sum up, in case with task1
, the following override of Task.Run
is inferred by compiler:
public static Task Run(Func<Task> function)
rather than:
public static Task Run(Action action)
That's because the task1
lambda has no natural code path out of the for
loop, so it may as well be a Func<Task>
lambda, despite it is not async
and it doesn't return anything. This is the option that compiler favors more than Action
. Then, the use of such override of Task.Run
is equivalent to this:
var task1 = Task.Factory.StartNew(new Func<Task>(() =>
{
for (var i = 0; ; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
})).Unwrap();
A nested task of type Task<Task>
is returned by Task.Factory.StartNew
, which gets unwrapped to Task
by Unwrap()
. Task.Run
is smart enough to do such unwrapping automatically for when it accepts Func<Task>
. The unwrapped promise-style task correctly propagates the cancellation status from its inner task, thrown as an OperationCanceledException
exception by the Func<Task>
lambda. This doesn't happen for task2
, which accepts an Action
lambda and doesn't create any inner tasks. The cancellation doesn't get propagated for task2
, because token
has not been associated with task2
via Task.Run
.
In the end, this may be a desired behavior for task1
(certainly not for task2
), but we don't want to create nested tasks behind the scene in either case. Moreover, this behavior for task1
may easily get broken by introducing a conditional break
out of the for
loop.
The correct code for task1
should be this:
var task1 = Task.Run(new Action(() =>
{
for (var i = 0; ; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
}), token);
这篇关于CancellationToken.ThrowIfCancellationRequested 之后的故障与取消任务状态的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!