通常我不回答问题,但这次我想引起一些我认为可能是一个模糊而又常见问题的注意。它是由this question触发的,从那以后我查看了我自己的旧代码,发现其中一些也受此影响。
下面的代码启动并等待两个任务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();
}
}
}
小提琴is here。输出:
{ task = task1, Message = The operation was canceled., Status = Canceled } { task = task2, Message = The operation was canceled., Status = Faulted }
为什么task1
的状态为Cancelled
,但task2
的状态为Faulted
?请注意,在这两种情况下我都会< em> not 将token
作为Task.Run
的第二个参数传递。
答案 0 :(得分:10)
这里有两个问题。首先,将CancellationToken
传递给Task.Run
API总是一个好主意,除了让它可用于任务的lambda之外。这样做会将令牌与任务相关联,对于token.ThrowIfCancellationRequested
触发的取消的正确传播至关重要。
然而,这并不能解释为什么task1
的取消状态仍然正确传播(task1.Status == TaskStatus.Canceled
),而task2
没有task2.Status == TaskStatus.Faulted
task1
}})。
现在,这可能是一种非常罕见的情况,其中聪明的C#类型推理逻辑可以与开发人员的意志对抗。我们详细讨论了here和here。总而言之,对于Task.Run
,编译器推断出以下public static Task Run(Func<Task> function)
的覆盖:
public static Task Run(Action action)
而不是:
task1
那是因为for
lambda没有Func<Task>
循环之外的自然代码路径,所以它也可能是async
lambda,尽管它不是Action
并且它不会返回任何内容。这是编译器偏爱Task.Run
以上的选项。然后,使用var task1 = Task.Factory.StartNew(new Func<Task>(() =>
{
for (var i = 0; ; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
})).Unwrap();
的这种覆盖等同于:
Task<Task>
Task.Factory.StartNew
返回Task
类型的嵌套任务,Unwrap()
将unwrapped传递给Task.Run
。 Func<Task>
is smart enough在接受OperationCanceledException
时自动进行此类展开。 解包的promise-style任务正确地从其内部任务传播取消状态,由Func<Task>
lambda作为task2
异常抛出。这不会发生在Action
,它接受task2
lambda并且不会创建任何内部任务。由于token
未通过task2
Task.Run
与task1
相关联,因此task2
未对其进行宣传。
最后,这可能是task1
的期望行为(当然不适用于break
),但我们不想在任何一种情况下在场景后面创建嵌套任务。此外,通过在for
循环中引入条件task1
,可能很容易打破var task1 = Task.Run(new Action(() =>
{
for (var i = 0; ; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
}), token);
的这种行为。
{{1}}的正确代码应为:
{{1}}