我目前正在学习如何使用Task
正确公开我们的库API的异步部分,以便它们可以更轻松,更好地用于客户。我决定使用the TaskCompletionSource
approach包围一个Task
,它不会被安排在线程池上(在这里不需要的实例中,因为它基本上只是一个计时器) 。这很好,但现在取消有点头疼。
该示例显示了基本用法,在令牌上注册委托,但比我的情况稍微复杂一点,更重要的是,我不确定如何处理{{1 }}。 The documentation表示要么只是返回并将任务状态切换为TaskCanceledException
,要么抛出RanToCompletion
(导致任务的结果为OperationCanceledException
)就可以了。但是,这些示例似乎只涉及或至少提及通过传递给Canceled
的委托启动的任务。
我的代码目前(大致)如下:
TaskFactory.StartNew
(在执行期间没有结果也没有可能的例外;我选择从这里开始的一个原因,而不是在图书馆中更复杂的地方。)
在当前表单中,当我在public Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
// Cancellation
token.Register(() => {
tcs.TrySetCanceled();
CancelAndCleanupFoo(foo);
});
RunFoo(foo, callback);
return tcs.Task;
}
上致电TrySetCanceled
时,如果等待返回的任务,我总会得到TaskCompletionSource
。我的猜测是,这是正常的行为(我希望是这样),并且当我想要使用取消时,我希望在呼叫周围包裹TaskCanceledException
/ try
。
如果我不使用catch
,那么我最终会在完成回调中运行,任务看起来就像是正常结束了。但我想如果用户想要区分正常完成的任务和被取消的任务,TrySetCanceled
几乎是确保这一点的副作用,对吗?
另一点我不太明白:Documentation suggests任何例外情况,甚至与取消有关的例外情况,都由TPL包含在TaskCanceledException
中。但是,在我的测试中,我总是直接得到AggregateException
,没有任何包装。我在这里遗漏了一些东西,还是记录得不好?
TL; DR:
TaskCanceledException
状态的任务,总是需要相应的例外,用户必须围绕异步调用包裹Canceled
/ try
能够发现那个,对吧?catch
也是预期和正常的,我在这里没有做错什么?答案 0 :(得分:3)
我总是建议人们阅读Cancellation in Managed Threads文档。它不完整;与大多数MSDN文档一样,它会告诉您可以做什么,而不是应该做什么。但它肯定比关于取消的dotnet文档更清晰。
该示例显示了基本用法
首先,请务必注意,示例代码中的取消仅取消任务 - 它不取消基础操作。我强烈建议您不要这样做。
如果您要取消操作,则需要更新RunFoo
以获取CancellationToken
(请参阅下文了解如何使用它):
public Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<AsyncCompletedEventArgs> callback = (sender, args) =>
{
if (args.Cancelled)
{
tcs.TrySetCanceled(token);
CleanupFoo(foo);
}
else
tcs.TrySetResult(null);
};
RunFoo(foo, token, callback);
return tcs.Task;
}
如果您无法取消foo
,则根本不会取消您的API支持:
public Task Run(IFoo foo) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
RunFoo(foo, callback);
return tcs.Task;
}
然后,调用者可以对任务执行可取消的等待,这对于这种情况来说是一种更合适的代码技术(因为等待被取消,而不是任务代表的操作)。执行&#34;可取消等待&#34;可以通过my AsyncEx.Tasks library完成,也可以编写自己的等效扩展方法。
文档说要么只返回并将任务状态切换到RanToCompletion,要么抛出OperationCanceledException(导致任务的结果被取消)就可以了。
是的,这些文档具有误导性。首先,请不要回来;您的方法将成功完成任务 - 表示操作已成功完成 - 实际上操作未成功完成。这可能适用于某些代码,但一般来说肯定不是一个好主意。
通常,回复CancellationToken
的正确方法是:
ThrowIfCancellationRequested
。对于CPU绑定代码,此选项更好。Register
注册取消回调。对于I / O绑定代码,此选项更好。请注意,必须处理注册!在您的特定情况下,您有一个不寻常的情况。在你的情况下,我会采取第三种方法:
token.IsCancellationRequested
;如果请求,则在AsyncCompletedEventArgs.Cancelled
设置为true
这在逻辑上等同于第一种正确的方式(定期调用ThrowIfCancellationRequested
),捕获异常,并将其转换为事件通知。没有例外。
如果我等待返回的任务,我总是得到一个TaskCanceledException。我的猜测是,这是正常的行为(我希望是这样),并且当我想使用取消时,我预计会围绕通话包装一个try / catch。
可以取消的任务的正确消费代码是将await
包装在try / catch 中并捕获OperationCanceledException
。由于各种原因(许多历史原因),某些API会导致OperationCanceledException
,而某些API会导致TaskCanceledException
。由于TaskCanceledException
派生自OperationCanceledException
,因此使用代码可以捕获更常见的异常。
但我想如果用户想要区分正常完成的任务和被取消的任务,[取消例外]几乎是确保这一点的副作用,对吧?
That's the accepted pattern,是的。
文档表明,任何异常,甚至是与取消相关的异常,都会被TPL包含在AggregateException中。
仅当您的代码同步阻止任务时才会出现这种情况。它应该首先应该避免做什么。因此,文档肯定会再次误导。
但是,在我的测试中,我总是直接得到TaskCanceledException,没有任何包装器。
await
避免使用AggregateException
包装。
更新,对于解释CleanupFoo
的评论是取消方法。
我首先建议您尝试在CancellationToken
启动的代码中直接使用RunFoo
;这种方法几乎肯定会更容易。
但是,如果您必须使用CleanupFoo
进行取消,那么您需要Register
它。您需要处理该注册,最简单的方法可能是将其拆分为两种不同的方法:
private Task DoRun(IFoo foo) {
var tcs = new TaskCompletionSource<object>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(null);
RunFoo(foo, callback);
return tcs.Task;
}
public async Task Run(IFoo foo, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<object>();
using (token.Register(() =>
{
tcs.TrySetCanceled(token);
CleanupFoo();
});
{
var task = DoRun(foo);
try
{
await task;
tcs.TrySetResult(null);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}
await tcs.Task;
}
正确地协调和传播结果 - 同时防止资源泄漏 - 非常尴尬。如果您的代码可以直接使用CancellationToken
,那么它会更清晰。
答案 1 :(得分:1)
从评论中,看起来你有一个接受IAnimation
的动画库,执行它(显然是异步的),然后发出信号表明它已完成。
这不是一个真正的任务,因为它不是一个必须在线程上运行的工作。它是一个异步操作,在.NET中使用Task对象公开。
此外,你实际上取消某事,你正在停止动画。这是一个完全正常的操作,所以它不应该抛出异常。如果你的方法返回一个解释动画是否完成的值,例如:
,那会更好public Task<bool> Run(IAnimation animation, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<bool>();
// Regular finish handler
EventHandler<EventArgs> callback = (sender, args) => tcs.TrySetResult(true);
// Cancellation
token.Register(() => {
CleanupFoo(animation);
tcs.TrySetResult(false);
});
RunFoo(animation, callback);
return tcs.Task;
}
运行动画的调用很简单:
var myAnimation = new SomeAnimation();
var completed = await runner.Run(myAnimation,token);
if (completed)
{
}
更新
这可以通过一些C#7技巧进一步改进。
例如,您可以使用本地函数,而不是使用回调和lambda。除了使代码更清晰之外,他们不每次调用代理时都会分配代理。此更改不需要客户端的C#7支持:
Task<bool> Run(IAnimation animation, CancellationToken token = default(CancellationToken)) {
var tcs = new TaskCompletionSource<bool>();
// Regular finish handler
void OnFinish (object sender, EventArgs args) => tcs.TrySetResult(true);
void OnStop(){
CleanupFoo(animation);
tcs.TrySetResult(false);
}
// Null-safe cancellation
token.Register(OnStop);
RunFoo(animation, OnFinish);
return tcs.Task;
}
您还可以返回更复杂的结果,例如包含Finished / Stopped标志的结果类型以及动画停止时的最终帧。如果你不想使用无意义的字段(为什么在动画完成后指定一个框架?),你可以返回一个实现例如IResult的Success类型或Stopped类型。
在C#7之前,您需要检查返回类型或使用重载来访问不同类型。通过模式匹配,您可以通过开关获得实际结果,例如:
interface IResult{}
public class Success:IResult{}
public class Stopped {
public int Frame{get;}
Stopped(int frame) { Frame=frame; }
}
....
var result=await Run(...);
switch (result)
{
case Success _ :
Console.WriteLine("Finished");
break;
case Stopped s :
Console.WriteLine($"Stopped at {s.Frame}");
break;
}
模式匹配实际上比类型检查更快。这要求客户端支持C#7。
答案 2 :(得分:1)
你正在做的事情很好 - 在将来,任务代表一些带有结果的操作,没有必要在另一个线程或类似的东西上运行任何东西。使用标准的取消方法,取消,而不是返回类似布尔值的东西,这是完全正常的。
回答你的问题:当你执行tcs.TrySetCanceled()
时,它会将任务移动到已取消状态(task.IsCancelled
将为真),此时不会抛出任何异常。但是当你await
这个任务时 - 它会注意到任务被取消,这就是TaskCancelledException
将被抛出的点。这里没有任何内容包含在聚合异常中,因为实际上没有任何内容可以包装 - TaskCancelledException
作为await
逻辑的一部分被抛出。现在,如果您要执行task.Wait()
之类的操作,那么它会将TaskCancelledException
包装成AggregateException
,如您所料。
请注意await
无论如何都会解包AggregateExceptions,因此您可能永远不会期望await task
抛出AggregateException - 如果出现多个异常,则只会抛出一个 - 其余的将被吞下。
现在,如果您使用取消令牌与常规任务 - 事情有点不同。当您执行token.ThrowIfCancellationRequested
之类的操作时,它会实际抛出OperationCancelledException
(请注意,它不是TaskCancelledException
,但TaskCancelledException
无论如何都是OperationCancelledException
的子类。然后,如果用于抛出此异常的CancellationToken
与启动时传递给任务的CancellationToken
相同(例如链接中的任务) - 任务将以相同的方式移动到“已取消”状态。这与代码中具有相同行为的tcs.TrySetCancelled
相同。如果令牌不匹配 - 任务将转到Faulted状态,就像抛出常规异常一样。