为什么抛出OperationCanceledException会得到不同的结果?

时间:2015-11-21 23:51:39

标签: c# task-parallel-library

我的同事玩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 (在这两种情况下都会导致“取消”结果)但无论如何我无法解释为什么会这样。

1 个答案:

答案 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(),这在此方案中通常需要。