为什么抛出 OperationCanceledException 会得到不同的结果?
Why throwing OperationCanceledException gets me different results?
我的同事玩过 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();
这会按预期打印 "Canceled",但是如果您只是像这样评论 while 行:
// ..
//while (true)
{
Thread.Sleep(300);
if (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException();
}
}
//..
你会得到 "Faulted"。我很清楚 ThrowIfCancellationRequested() 方法,我应该在 OperationCanceledException 的构造函数中传递 cancellationToken (这会导致 "Canceled"导致两种情况)但无论如何我无法解释为什么会发生这种情况。
恕我直言,您所询问的行为更恰当地被质疑为 "why does the task status transition to Canceled
when the while
loop is present?"。我这样说是因为代码的自然阅读是它应该 always 过渡到 Faulted
相反。
通常情况下,取消的工作方式是,只有当 OperationCanceledException
构造函数传递给 [=17= 相同的 CancellationToken
实例时,您才会获得 Canceled
状态] 方法。否则,任务会在出现任何异常时转换为 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
语句,编译器(出于某种原因)已将方法的 return 类型推断为 Task
而不是 void
.我承认,我不知道为什么会这样;该方法不是 async
,没关系它是否有任何 await
,并且 lambda 肯定不是一个计算为 Task
值的简单表达式。但即便如此,编译器还是决定将此方法实现为 returning 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
循环之后添加一个(无法访问的)return
语句。在任何一种情况下,这都会导致匿名方法具有 void
的 return 类型,从而导致调用 Run()
的 Action
重载,这将 OperationCanceledException
视为任何其他,将任务转换为 Faulted
状态。
当然,如果您将 cancellationToken
值传递给 OperationCanceledException
构造函数,或者调用 cancellationToken.ThrowIfCancellationRequested()
而不是显式检查和抛出,异常本身将正确地指示它根据传递给 Run()
方法的 CancellationToken
被抛出,因此任务将转换为 Canceled
,正如在这种情况下通常需要的那样。
我的同事玩过 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();
这会按预期打印 "Canceled",但是如果您只是像这样评论 while 行:
// ..
//while (true)
{
Thread.Sleep(300);
if (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException();
}
}
//..
你会得到 "Faulted"。我很清楚 ThrowIfCancellationRequested() 方法,我应该在 OperationCanceledException 的构造函数中传递 cancellationToken (这会导致 "Canceled"导致两种情况)但无论如何我无法解释为什么会发生这种情况。
恕我直言,您所询问的行为更恰当地被质疑为 "why does the task status transition to Canceled
when the while
loop is present?"。我这样说是因为代码的自然阅读是它应该 always 过渡到 Faulted
相反。
通常情况下,取消的工作方式是,只有当 OperationCanceledException
构造函数传递给 [=17= 相同的 CancellationToken
实例时,您才会获得 Canceled
状态] 方法。否则,任务会在出现任何异常时转换为 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
语句,编译器(出于某种原因)已将方法的 return 类型推断为 Task
而不是 void
.我承认,我不知道为什么会这样;该方法不是 async
,没关系它是否有任何 await
,并且 lambda 肯定不是一个计算为 Task
值的简单表达式。但即便如此,编译器还是决定将此方法实现为 returning 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
循环之后添加一个(无法访问的)return
语句。在任何一种情况下,这都会导致匿名方法具有 void
的 return 类型,从而导致调用 Run()
的 Action
重载,这将 OperationCanceledException
视为任何其他,将任务转换为 Faulted
状态。
当然,如果您将 cancellationToken
值传递给 OperationCanceledException
构造函数,或者调用 cancellationToken.ThrowIfCancellationRequested()
而不是显式检查和抛出,异常本身将正确地指示它根据传递给 Run()
方法的 CancellationToken
被抛出,因此任务将转换为 Canceled
,正如在这种情况下通常需要的那样。