你如何捕获CancellationToken.Register回调exception?

我使用异步I / O与HID设备通信,我想在超时时抛出一个可捕获的exception。 我有以下读取方法:

public async Task Read( byte[] buffer, int? size=null ) { size = size ?? buffer.Length; using( var cts = new CancellationTokenSource() ) { cts.CancelAfter( 1000 ); cts.Token.Register( () => { throw new TimeoutException( "read timeout" ); }, true ); try { var t = stream.ReadAsync( buffer, 0, size.Value, cts.Token ); await t; return t.Result; } catch( Exception ex ) { Debug.WriteLine( "exception" ); return 0; } } } 

从Token的回调抛出的exception不被任何try / catch块捕获,我不知道为什么。 我以为它会被等待,但事实并非如此。 有没有办法捕获此exception(或使其可由Read()的调用者捕获)?

编辑:所以我重新阅读了msdn上的文档,它说:“委托生成的任何exception都将传播出此方法调用。”

我不确定“传播出这个方法调用”是什么意思,因为即使我将.Register()调用移到try块中,exception仍然没有被捕获。

编辑:所以我重新阅读了msdn上的文档,它说:“委托生成的任何exception都将传播出此方法调用。”

我不确定“传播出这个方法调用”是什么意思,因为即使我将.Register()调用移到try块中,exception仍然没有被捕获。

这意味着你的取消回调的调用者(.NET Runtime中的代码)将不会尝试捕获你可能抛出的任何exception,因此它们将在你的回调之外,在任何堆栈帧和同步上下文中传播调用了回调。 这可能会使应用程序崩溃,因此您应该真正处理回调中的所有非致命exception。 将其视为事件处理程序。 毕竟,可能有多个回调用ct.Register()注册,每个回调都可能抛出。 应该传播哪个例外呢?

因此, 不会捕获此类exception并将其传播到令牌的“客户端”端(即,调用CancellationToken.ThrowIfCancellationRequested的代码)。

如果您需要区分用户取消(例如,“停止”按钮)和超时,这是抛出TimeoutException的另一种方法:

 public async Task Read( byte[] buffer, int? size=null, CancellationToken userToken) { size = size ?? buffer.Length; using( var cts = CancellationTokenSource.CreateLinkedTokenSource(userToken)) { cts.CancelAfter( 1000 ); try { var t = stream.ReadAsync( buffer, 0, size.Value, cts.Token ); try { await t; } catch (OperationCanceledException ex) { if (ex.CancellationToken == cts.Token) throw new TimeoutException("read timeout", ex); throw; } return t.Result; } catch( Exception ex ) { Debug.WriteLine( "exception" ); return 0; } } } 

我个人更喜欢将Cancellation逻辑包装到它自己的方法中。

例如,给定一个扩展方法,如:

 public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(); using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) { if (task != await Task.WhenAny(task, tcs.Task)) { throw new OperationCanceledException(cancellationToken); } } return task.Result; } 

您可以将方法简化为:

 public async Task Read( byte[] buffer, int? size=null ) { size = size ?? buffer.Length; using( var cts = new CancellationTokenSource() ) { cts.CancelAfter( 1000 ); try { return await stream.ReadAsync( buffer, 0, size.Value, cts.Token ).WithCancellation(cts.Token); } catch( OperationCanceledException cancel ) { Debug.WriteLine( "cancelled" ); return 0; } catch( Exception ex ) { Debug.WriteLine( "exception" ); return 0; } } } 

在这种情况下,由于您的唯一目标是执行超时,因此您可以使这更简单:

 public static async Task TimeoutAfter(this Task task, TimeSpan timeout) { if (task != await Task.WhenAny(task, Task.Delay(timeout))) { throw new TimeoutException(); } return task.Result; // Task is guaranteed completed (WhenAny), so this won't block } 

那你的方法可以是:

 public async Task Read( byte[] buffer, int? size=null ) { size = size ?? buffer.Length; try { return await stream.ReadAsync( buffer, 0, size.Value, cts.Token ).TimeoutAfter(TimeSpan.FromSeconds(1)); } catch( TimeoutException timeout ) { Debug.WriteLine( "Timed out" ); return 0; } catch( Exception ex ) { Debug.WriteLine( "exception" ); return 0; } } 

使用CancellationToken.Register()注册的回调的exception处理很复杂。 🙂

令牌在回叫注册前被取消

如果在注册取消回调之前取消了取消令牌,则将通过CancellationToken.Register()同步执行回调。 如果回调引发exception,那么该exception将从Register()传播,因此可以使用try...catch它。

这种传播就是你引用的语句所指的。 对于上下文,这是引用来自的完整段落。

如果此令牌已处于已取消状态,则该委托将立即并同步运行。 委托生成的任何exception都将传播出此方法调用。

“此方法调用”是指对CancellationToken.Register()的调用。 (对于被这一段感到困惑,不要感到难过。当我第一次看到它时,我也很困惑。)

回调注册后令牌被取消

通过调用CancellationTokenSource.Cancel()取消

通过调用此方法取消令牌时,取消回调由它同步执行。 根据所使用的Cancel()的重载,可以:

  • 将运行所有取消回调。 引发的任何exception都将合并到一个从Cancel()传播的AggregateException中。
  • 除非并且直到抛出exception,否则将运行所有取消回调。 如果回调抛出exception,该exception将从Cancel()传播出来(不包含在AggregateException ),并且将跳过任何未执行的取消回调。

在任何一种情况下,比如CancellationToken.Register() ,可以使用普通的try...catch来捕获exception。

CancellationTokenSource.CancelAfter()取消

此方法启动倒数计时器然后返回。 当计时器到达零时,计时器使取消过程在后台运行。

由于CancelAfter()实际上没有运行取消过程,因此取消回调exception不会传播出来。 如果你想观察它们,你需要恢复使用某些方法来拦截未处理的exception。

在您的情况下,由于您正在使用CancelAfter() ,因此拦截未处理的exception是您唯一的选择。 try...catch不起作用。

建议

为了避免这些复杂性,在可能的情况下不允许取消回调抛出exception。

进一步阅读

  • CancellationTokenSource.Cancel() – 讨论如何处理取消回调exception
  • 了解取消回调 – 我最近就此主题撰写的博客文章