nos*_*tio 17 .net c# multithreading task-parallel-library async-await
通常我不会回答问题,但这次我想引起一些我认为可能是一个模糊而又常见的问题的注意.它是由这个问题引发的,从那以后我查看了我自己的旧代码,发现其中一些也受此影响.
下面的代码开始,等待着两个任务,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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
小提琴在这里.输出:
{ task = task1, Message = The operation was canceled., Status = Canceled }
{ task = task2, Message = The operation was canceled., Status = Faulted }
为什么状态task1是Cancelled,但状态task2是Faulted?请注意,在这两种情况下,我都不会将token第二个参数传递给Task.Run.
nos*_*tio 11
这里有两个问题.首先,传递CancellationToken给Task.RunAPI 总是一个好主意,除了使它可用于任务的lambda.这样做会将令牌与任务相关联,这对于正确传播由此触发的取消至关重要token.ThrowIfCancellationRequested.
然而,这并不能解释为什么取消状态task1仍然正确传播(task1.Status == TaskStatus.Canceled),而不是task2(task2.Status == TaskStatus.Faulted).
现在,这可能是一种非常罕见的情况,其中聪明的C#类型推断逻辑可以违背开发人员的意愿.这里和这里详细讨论了它.总而言之,如果是,编译器推断出task1以下覆盖Task.Run:
public static Task Run(Func<Task> function)
Run Code Online (Sandbox Code Playgroud)
而不是:
public static Task Run(Action action)
Run Code Online (Sandbox Code Playgroud)
这是因为task1lambda没有自然的代码路径for,所以它也可能是一个Func<Task>lambda,尽管它不是async,它不会返回任何东西.这是编译器偏爱的选项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();
Run Code Online (Sandbox Code Playgroud)
类型的嵌套任务Task<Task>是返回Task.Factory.StartNew,这被拆开来Task用Unwrap().Task.Run 足够聪明,可以在接受时自动进行此类展开Func<Task>.未展开的promise-style任务正确地从其内部任务传播取消状态,OperationCanceledException由Func<Task>lambda 作为异常抛出.这不会发生task2,它接受Actionlambda并且不会创建任何内部任务.取消不会传播task2,因为token没有与task2via 关联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);
Run Code Online (Sandbox Code Playgroud)