CancellationToken.ThrowIfCancellationRequested后出现故障与已取消的任务状态
通常我不回答问题,但这次我想引起一些关注,我认为这可能是一个模糊而又常见的问题。 它是由这个问题触发的,从那时起我查看了我自己的旧代码,发现其中一些也受到了影响。
下面的代码启动并等待两个任务, task1
和task2
,几乎完全相同。 task1
与task2
不同之处在于它运行永无止境的循环。 对于执行CPU限制工作的一些现实场景,这两种情况都非常典型。
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(); } } }
小提琴在这里 。 输出:
{task = task1,Message =操作已取消。,状态=已取消} {task = task2,Message =操作被取消。,Status = Faulted}
为什么task1
的状态为Cancelled
,但task2
的状态是Faulted
? 请注意,在这两种情况下,我都不会将token
作为第二个参数传递给Task.Run
。
这里有两个问题。 首先,将CancellationToken
传递给Task.Run
API总是一个好主意,除了使它可用于任务的lambda。 这样做会将令牌与任务相关联,对于由token.ThrowIfCancellationRequested
触发的取消的正确传播至关重要。
但是,这并不能解释为什么task1.Status == TaskStatus.Canceled
的取消状态仍然正确传播( task1.Status == TaskStatus.Canceled
),而不是task2
( task2.Status == TaskStatus.Faulted
)。
现在,这可能是一种非常罕见的情况,其中聪明的C#类型推断逻辑可以违背开发人员的意愿。 这里和这里详细讨论了它。 总而言之,对于Task.Run
,编译器推断出以下Task.Run
重写:
public static Task Run(Func function)
而不是:
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); // simulate work item #i token.ThrowIfCancellationRequested(); } })).Unwrap();
Task.Factory.StartNew
返回Task
类型的嵌套任务,它通过Unwrap()
解包到Task
。 Task.Run
非常聪明,可以在接受Func
时自动进行此类展开。 展开的promise样式任务正确地从其内部任务传播取消状态 ,由Func
lambda作为OperationCanceledException
exception抛出。 对于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); // simulate work item #i token.ThrowIfCancellationRequested(); } }), token);