TaskCancellationException 如何避免成功控制流的异常?
TaskCancellationException how to avoid the exception on success control flow?
在我们的应用程序中,我们经常使用 async / await 和 Tasks。因此它确实经常使用 Task.Run,有时使用内置的 CancellationToken
.
取消支持
public Task DoSomethingAsync(CancellationToken cancellationToken)
{
return Task.Run(() =>
{
while (true)
{
if (cancellationToken.IsCancellationRequested) break;
//do some work
}
}, cancellationToken);
}
如果我现在使用 CancellationToken 取消执行,执行会在下一个循环开始时停止,或者如果任务根本没有启动,它会抛出异常(Task.Run 内的 TaskCanceledException)。现在的问题是,为什么 Task.Run 使用 Exception 来控制成功取消,而不是只返回一个已完成的 Task。 MS 没有遵守 "Do NOT use exceptions to control execution flow" 规则有什么具体原因吗?
以及如何避免在完全无用的 try catch (TaskCancelledException) 块中对支持取消(很多)的每个方法进行装箱?
好吧,在非常简单的场景中您无法真正看出区别 - 您实际上并没有使用 Task
的结果,并且您不需要通过复杂的方法传播取消调用堆栈。
首先,您的 Task
可能 return 是一个值。当操作被取消时,你return做什么?
其次,在您取消的任务之后可能还有其他任务。您可能希望在方便时通过其他任务传播取消。
异常传播。任务取消与此用法中的 Thread.Abort
几乎相同 - 当您发出 Thread.Abort
时,ThreadAbortException
用于确保您一路放松回到顶部。否则,您的所有方法都必须检查它们调用的每个方法的结果,检查它们是否被取消,并在需要时 return 自己 - 我们已经看到人们会忽略错误 return老派 C 中的值 :)
最后,任务取消,就像线程中止一样,是一种例外情况。它已经涉及同步、堆栈展开等
但是,这并不意味着您必须使用 try-catch
来捕获异常 - 您可以使用任务状态。例如,您可以使用这样的辅助函数:
public static Task<T> DefaultIfCanceled<T>(this Task<T> @this, T defaultValue = default(T))
{
return
@this.ContinueWith
(
t =>
{
if (t.IsCanceled) return defaultValue;
return t.Result;
}
);
}
您可以将其用作
await SomeAsync().DefaultIfCanceled();
当然,需要注意的是,noöne 强迫您使用这种取消方法 - 它只是为了方便而提供的。例如,您可以使用自己的放大类型来保留取消信息,并手动处理取消。但是当你开始这样做时,你会发现使用异常处理取消的原因——在命令式代码中这样做很痛苦,所以你要么白白浪费大量精力,要么转而使用更实用的编程方式(来吧,我们有 cookie!*)。
(*) 免责声明:我们实际上没有 cookie。但是你可以自己制作!
抛出异常是有目的的,因为社区中的其他人已经指出了这一点。
但是,如果您想对 TaskCanceledException
行为有更多的控制,并且仍然将逻辑隔离到一个地方,您可以实现一个扩展方法来扩展 Task
来处理取消,比如这 -
public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
await Task.Run(() =>
{
while (true)
{
if (cancellationToken.IsCancellationRequested) break;
//do some work
}
}).
WithCancellation(cancellationToken,false); // pass the cancellation token to extension funciton instead to run
}
static class TaskCacellationHelper
{
private struct Void { } // just to support TaskCompletionSource class.
public static async Task WithCancellation(this Task originalTask, CancellationToken ct, bool suppressCancellationExcetion)
{
// Create a Task that completes when the CancellationToken is canceled
var cancelTask = new TaskCompletionSource<Void>();
// When the CancellationToken is canceled, complete the Task
using (ct.Register(
t => ((TaskCompletionSource<Void>)t).TrySetResult(new Void()), cancelTask))
{
// Create a Task that completes when either the original or
// CancellationToken Task completes
Task any = await Task.WhenAny(originalTask, cancelTask.Task);
// If any Task completes due to CancellationToken, throw OperationCanceledException
if (any == cancelTask.Task)
{
//
if (suppressCancellationExcetion == false)
{
ct.ThrowIfCancellationRequested();
}
else
{
Console.WriteLine("Cancelled but exception supressed");
}
}
}
// await original task. Incase of cancellation your logic will break the while loop
await originalTask;
}
}
在我们的应用程序中,我们经常使用 async / await 和 Tasks。因此它确实经常使用 Task.Run,有时使用内置的 CancellationToken
.
public Task DoSomethingAsync(CancellationToken cancellationToken)
{
return Task.Run(() =>
{
while (true)
{
if (cancellationToken.IsCancellationRequested) break;
//do some work
}
}, cancellationToken);
}
如果我现在使用 CancellationToken 取消执行,执行会在下一个循环开始时停止,或者如果任务根本没有启动,它会抛出异常(Task.Run 内的 TaskCanceledException)。现在的问题是,为什么 Task.Run 使用 Exception 来控制成功取消,而不是只返回一个已完成的 Task。 MS 没有遵守 "Do NOT use exceptions to control execution flow" 规则有什么具体原因吗?
以及如何避免在完全无用的 try catch (TaskCancelledException) 块中对支持取消(很多)的每个方法进行装箱?
好吧,在非常简单的场景中您无法真正看出区别 - 您实际上并没有使用 Task
的结果,并且您不需要通过复杂的方法传播取消调用堆栈。
首先,您的 Task
可能 return 是一个值。当操作被取消时,你return做什么?
其次,在您取消的任务之后可能还有其他任务。您可能希望在方便时通过其他任务传播取消。
异常传播。任务取消与此用法中的 Thread.Abort
几乎相同 - 当您发出 Thread.Abort
时,ThreadAbortException
用于确保您一路放松回到顶部。否则,您的所有方法都必须检查它们调用的每个方法的结果,检查它们是否被取消,并在需要时 return 自己 - 我们已经看到人们会忽略错误 return老派 C 中的值 :)
最后,任务取消,就像线程中止一样,是一种例外情况。它已经涉及同步、堆栈展开等
但是,这并不意味着您必须使用 try-catch
来捕获异常 - 您可以使用任务状态。例如,您可以使用这样的辅助函数:
public static Task<T> DefaultIfCanceled<T>(this Task<T> @this, T defaultValue = default(T))
{
return
@this.ContinueWith
(
t =>
{
if (t.IsCanceled) return defaultValue;
return t.Result;
}
);
}
您可以将其用作
await SomeAsync().DefaultIfCanceled();
当然,需要注意的是,noöne 强迫您使用这种取消方法 - 它只是为了方便而提供的。例如,您可以使用自己的放大类型来保留取消信息,并手动处理取消。但是当你开始这样做时,你会发现使用异常处理取消的原因——在命令式代码中这样做很痛苦,所以你要么白白浪费大量精力,要么转而使用更实用的编程方式(来吧,我们有 cookie!*)。
(*) 免责声明:我们实际上没有 cookie。但是你可以自己制作!
抛出异常是有目的的,因为社区中的其他人已经指出了这一点。
但是,如果您想对 TaskCanceledException
行为有更多的控制,并且仍然将逻辑隔离到一个地方,您可以实现一个扩展方法来扩展 Task
来处理取消,比如这 -
public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
await Task.Run(() =>
{
while (true)
{
if (cancellationToken.IsCancellationRequested) break;
//do some work
}
}).
WithCancellation(cancellationToken,false); // pass the cancellation token to extension funciton instead to run
}
static class TaskCacellationHelper
{
private struct Void { } // just to support TaskCompletionSource class.
public static async Task WithCancellation(this Task originalTask, CancellationToken ct, bool suppressCancellationExcetion)
{
// Create a Task that completes when the CancellationToken is canceled
var cancelTask = new TaskCompletionSource<Void>();
// When the CancellationToken is canceled, complete the Task
using (ct.Register(
t => ((TaskCompletionSource<Void>)t).TrySetResult(new Void()), cancelTask))
{
// Create a Task that completes when either the original or
// CancellationToken Task completes
Task any = await Task.WhenAny(originalTask, cancelTask.Task);
// If any Task completes due to CancellationToken, throw OperationCanceledException
if (any == cancelTask.Task)
{
//
if (suppressCancellationExcetion == false)
{
ct.ThrowIfCancellationRequested();
}
else
{
Console.WriteLine("Cancelled but exception supressed");
}
}
}
// await original task. Incase of cancellation your logic will break the while loop
await originalTask;
}
}