你如何捕获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
- 了解取消回调 – 我最近就此主题撰写的博客文章