我的同事玩TPL并取消任务。他向我展示了以下代码:
var cancellationToken = cts.Token;
var task = Task.Run(() =>
{
while (true)
{
Thread.Sleep(300);
if (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException();
}
}
}, cancellationToken)
.ContinueWith(t => {
Console.WriteLine(t.Status);
});
Thread.Sleep(200);
cts.Cancel();
这会按预期打印“已取消”,但如果你只是评论,这样的行:
// ..
//while (true)
{
Thread.Sleep(300);
if (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException();
}
}
//..
你会得到“故障”。我很清楚 ThrowIfCancellationRequested()方法,我应该在OperationCanceledException的构造函数中传递 cancellationToken (在这两种情况下都会导致“取消”结果)但无论如何我无法解释为什么会这样。
答案 0 :(得分:3)
您所询问的行为会更恰当地被质疑为“当Canceled
循环存在时,为什么任务状态转换为while
?”。我这样说是因为代码的自然读取应该始终转换为Faulted
。
通常,取消的方式是,如果Canceled
构造函数传递给传递给OperationCanceledException
方法的同一CancellationToken
实例,则只能获得Task.Run()
状态。否则,任务在任何异常时都会转换为Faulted
。
这不是你添加while
循环时发生的事情,至少可以说是奇怪的。那么,为什么会发生这种奇怪的事情?
嗯,在编译器生成的代码中(至少部分地)找到了答案。当存在while
循环时,这是循环的IL(此IL还包括对Console.WriteLine()
的诊断调用,但是恰好是您发布的代码):
.method public hidebysig instance class [mscorlib]System.Threading.Tasks.Task
'<Main>b__1'() cil managed
{
// Code size 67 (0x43)
.maxstack 2
.locals init (class [mscorlib]System.Threading.Tasks.Task V_0,
bool V_1)
IL_0000: nop
IL_0001: br.s IL_003f
IL_0003: nop
IL_0004: ldstr "sleeping"
IL_0009: call void [mscorlib]System.Console::WriteLine(string)
IL_000e: nop
IL_000f: ldc.i4 0x12c
IL_0014: call void [mscorlib]System.Threading.Thread::Sleep(int32)
IL_0019: nop
IL_001a: ldarg.0
IL_001b: ldflda valuetype [mscorlib]System.Threading.CancellationToken TestSO33850046CancelVsFaulted.Program/'<>c__DisplayClass3'::cancellationToken
IL_0020: call instance bool [mscorlib]System.Threading.CancellationToken::get_IsCancellationRequested()
IL_0025: ldc.i4.0
IL_0026: ceq
IL_0028: stloc.1
IL_0029: ldloc.1
IL_002a: brtrue.s IL_003e
IL_002c: nop
IL_002d: ldstr "throwing"
IL_0032: call void [mscorlib]System.Console::WriteLine(string)
IL_0037: nop
IL_0038: newobj instance void [mscorlib]System.OperationCanceledException::.ctor()
IL_003d: throw
IL_003e: nop
IL_003f: ldc.i4.1
IL_0040: stloc.1
IL_0041: br.s IL_0003
} // end of method '<>c__DisplayClass3'::'<Main>b__1'
请注意,即使该方法没有return
语句,编译器也会(由于某种原因)将方法的返回类型推断为Task
而不是void
。我承认,我不知道为什么会这样;该方法不是async
,更不用说它有await
,并且lambda当然不是一个评估Task
值的简单表达式。但即便如此,编译器决定将此方法实现为返回Task
。
这又会影响调用Task.Run()
方法重载。它将调用Task.Run(Action, CancellationToken)
,而不是调用Task.Run(Func<Task>, CancellationToken)
。事实证明,这两种方法的实现与另一种方法截然不同。虽然Action
重载只是创建一个新的Task
对象并启动它,但the Func<Task>
overload将创建的任务包装在UnwrapPromise<T>
对象中,并向其构造函数传递一个标志,告诉它明确地留意OperationCanceledException
并将其视为Canceled
结果,而不是Faulted
。
如果注释掉while
,编译器会将匿名方法实现为返回类型为void
。同样,如果在return
循环后添加(不可到达)while
语句。在任何一种情况下,这都会导致匿名方法的返回类型为void
,从而导致Action
的{{1}}重载被调用,这会像其他任何过渡一样处理Run()
将任务改为OperationCanceledException
状态。
当然,如果您将Faulted
值传递给cancellationToken
构造函数,或者调用OperationCanceledException
而不是显式检查和抛出,则异常本身会正确地指示它被抛出根据传递给cancellationToken.ThrowIfCancellationRequested()
方法的CancellationToken
,因此任务将转换为Run()
,这在此方案中通常需要。