9

我正在使用异步 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;
        }
    }
}

令牌回调抛出的异常没有被任何 try/catch 块捕获,我不知道为什么。我以为它会在等待时被抛出,但事实并非如此。有没有办法捕捉这个异常(或者让它可以被 Read() 的调用者捕捉到)?

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

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

4

3 回答 3

15

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

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

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;
    }
}
于 2014-05-12T21:05:04.837 回答
10

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

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

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

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

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

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;
        }
    }
}
于 2014-05-12T23:29:40.530 回答
5

注册的回调的异常处理CancellationToken.Register()很复杂。:-)

回调注册前取消令牌

如果在注册取消回调之前取消了取消令牌,回调将由 同步执行CancellationToken.Register()。如果回调引发异常,则该异常将被传播Register(),因此可以使用try...catch围绕它的 a 来捕获。

这种传播就是您引用的陈述所指的内容。对于上下文,这是该引用来自的完整段落。

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

“此方法调用”是指对CancellationToken.Register(). (不要因为被这一段弄糊涂而感到难过。前阵子第一次读的时候,我也很困惑。)

回调注册后令牌被取消

通过调用CancellationTokenSource.Cancel() 取消

当调用该方法取消令牌时,取消回调由它同步执行。根据使用的过载情况Cancel(),可以:

  • 所有取消回调都将运行。引发的任何异常都将组合成一个AggregateExceptionCancel().
  • 所有取消回调都将运行,除非并且直到抛出异常。如果回调抛出异常,则该异常将被传播出去Cancel()(不包装在 an 中AggregateException),并且将跳过任何未执行的取消回调。

在任何一种情况下,例如,都可以使用CancellationToken.Register()普通函数来捕获异常。try...catch

CancellationTokenSource.CancelAfter() 取消

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

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

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

推荐

为了避免这些复杂性,尽可能不要让取消回调抛出异常。

延伸阅读

于 2018-02-09T19:31:51.310 回答