bj0*_*bj0 9 c# task-parallel-library cancellationtokensource
我正在使用异步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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
从Token的回调抛出的异常不会被任何try/catch块捕获,我不知道为什么.我以为它会被等待,但事实并非如此.有没有办法捕获这个异常(或让它由Read()的调用者捕获)?
编辑: 所以我重新阅读msdn上的文档,它说:"委托生成的任何异常都将从此方法调用中传播出来."
我不确定"传播出这个方法调用"是什么意思,因为即使我将.Register()调用移到try块中,异常仍然没有被捕获.
Ree*_*sey 10
我个人更喜欢将Cancellation逻辑包装到它自己的方法中.
例如,给定一个扩展方法,如:
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;
}
Run Code Online (Sandbox Code Playgroud)
您可以将方法简化为:
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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,由于您的唯一目标是执行超时,因此您可以使这更简单:
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
}
Run Code Online (Sandbox Code Playgroud)
那你的方法可以是:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
nos*_*tio 10
编辑:所以我重新阅读msdn上的文档,它说:"委托生成的任何异常都将从此方法调用中传播出来."
我不确定"传播出这个方法调用"是什么意思,因为即使我将.Register()调用移到try块中,异常仍然没有被捕获.
这意味着你的取消回调的调用者(.NET Runtime中的代码)将不会尝试捕获你可能抛出的任何异常,因此它们将在你的回调之外,在任何堆栈帧和同步上下文中传播调用了回调.这可能会使应用程序崩溃,因此您应该真正处理回调中的所有非致命异常.将其视为事件处理程序.毕竟,可能会注册多个回调ct.Register(),并且每个回调都可能抛出.应该传播哪个例外呢?
因此,不会捕获此类异常并将其传播到令牌的"客户端"侧(即,调用到的代码CancellationToken.ThrowIfCancellationRequested).
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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
注册回调的异常处理CancellationToken.Register()很复杂。:-)
如果在注册取消回调之前取消了取消令牌,则该回调将由 同步执行CancellationToken.Register()。如果回调引发异常,则该异常将从中传播Register(),因此可以使用try...catch它周围的 a 捕获该异常。
这种传播就是您引用的声明所指的内容。作为上下文,以下是该引用的完整段落。
如果此令牌已处于取消状态,则委托将立即同步运行。委托生成的任何异常都将从此方法调用中传播出去。
“此方法调用”是指对CancellationToken.Register(). (不要因为被这一段弄糊涂而感到难过。不久前我第一次读到它时,我也很困惑。)
当调用该方法取消token时,会同步执行取消回调。根据Cancel()所使用的过载,可以:
AggregateException引发的任何异常都将合并到从Cancel().Cancel()(不包含在 中AggregateException),并且将跳过任何未执行的取消回调。在任何一种情况下,例如CancellationToken.Register(),都可以使用正常try...catch来捕获异常。
该方法启动一个倒计时器然后返回。当计时器达到零时,计时器会导致取消进程在后台运行。
由于CancelAfter()实际上并不运行取消过程,因此取消回调异常不会从中传播出来。如果您想观察它们,则需要恢复使用某些拦截未处理异常的方法。
在您的情况下,由于您正在使用CancelAfter(),因此拦截未处理的异常是您唯一的选择。try...catch行不通的。
为了避免这些复杂性,请尽可能不允许取消回调抛出异常。