我使用异步I / O与HID设备通信,我想在超时时抛出一个可捕获的异常。我有以下读取方法:
public async Task<int> 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的回调抛出的异常没有被任何try / catch块捕获,我不知道为什么。我以为它会被等待,但事实并非如此。有没有办法捕获这个异常(或让它由Read()的调用者捕获?)
修改 所以我在msdn重新阅读了文档,并说“委托生成的任何异常都将从此方法调用中传播出来。”
我不确定“传播出这个方法调用”意味着什么,因为即使我将.Register()调用移到try块中,异常仍然没有被捕获。
答案 0 :(得分:10)
编辑:所以我重新阅读了msdn上的文档,它说&#34;任何例外 委托生成将从此方法调用中传播。&#34;
我不确定&#34;传播出来的方法调用&#34;, 因为即使我将.Register()调用移动到try块中也是如此 异常仍未被发现。
这意味着取消回调的调用者(.NET Runtime中的代码)不会尝试捕获您可能抛出的任何异常,因此它们将在您的回调之外传播,无论是什么堆栈帧和同步上下文调用了回调。这可能会使应用程序崩溃,因此您应该真正处理回调中的所有非致命异常。 将其视为事件处理程序。毕竟,可能会有ct.Register()
注册多个回调,并且每个回调都可能抛出。应该传播哪个例外?
因此,此类异常将不被捕获并传播到&#34;客户端&#34;令牌的一侧(即,调用CancellationToken.ThrowIfCancellationRequested
的代码)。
如果你需要区分用户取消(例如,&#34;停止&#34;按钮)和超时,那么这里可以选择抛出TimeoutException
的方法:
public async Task<int> 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;
}
}
}
答案 1 :(得分:9)
我个人更喜欢将取消逻辑包装到它自己的方法中。
例如,给定一个扩展方法,如:
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task))
{
throw new OperationCanceledException(cancellationToken);
}
}
return task.Result;
}
您可以将方法简化为:
public async Task<int> 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<T> TimeoutAfter<T>(this Task<T> 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<int> 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;
}
}
答案 2 :(得分:2)
使用CancellationToken.Register()
注册的回调的异常处理很复杂。 : - )
如果在注册取消回叫之前取消了取消令牌,则回拨将由CancellationToken.Register()
同步执行。如果回调引发异常,则该异常将从Register()
传播,因此可以使用try...catch
围绕它进行捕获。
此传播是您引用的语句所指的内容。对于上下文,这是引用来自的完整段落。
如果此令牌已处于取消状态,则该委托将为 立即和同步运行。代表的任何例外 generate将从此方法调用中传播。
“此方法调用”是指对CancellationToken.Register()
的调用。 (对于被这一段感到困惑,不要感到难过。当我第一次读它时,我也很困惑。)
通过调用此方法取消令牌时,取消回调由它同步执行。根据所使用的Cancel()
的过载,可以:
AggregateException
,该CancellationTokenSource.CancelAfter()会从Cancel()
传播出来。Cancel()
传播出来(不包含在AggregateException
中),并且将跳过任何未执行的取消回调。在任何一种情况下,如CancellationToken.Register()
,可以使用普通try...catch
来捕获异常。
此方法启动倒数计时器然后返回。当计时器到达零时,计时器使取消过程在后台运行。
由于CancelAfter()
实际上没有运行取消过程,因此取消回调异常不会传播出来。如果你想观察它们,你需要恢复使用一些方法来拦截未处理的异常。
在您的情况下,由于您使用的是CancelAfter()
,因此拦截未处理的异常是您唯一的选择。 try...catch
无效。
为了避免这些复杂性,在可能的情况下不允许取消回调抛出异常。