如何捕捉CancellationToken.注册回调异常

本文关键字:回调 异常 注册 CancellationToken 何捕捉 | 更新日期: 2023-09-27 18:02:53

我正在使用异步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块捕获,我不确定为什么。我以为它会在await中被抛出,但事实并非如此。是否有方法捕获此异常(或使其可被Read()的调用者捕获)?

编辑:

所以我重读了msdn处的文档,它说"委托生成的任何异常都会从这个方法调用传播出去"

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

如何捕捉CancellationToken.注册回调异常

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

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

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;
    }
}
编辑:所以我在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;
        }
    }
}

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

令牌在回调注册前被取消

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

这个传播就是你引用的语句所指的。为了了解上下文,以下是这句话的完整段落。

如果此令牌已处于取消状态,则委托将处于取消状态立即同步运行。任何例外,委托将从此方法调用中传播。

"This method call"指的是对CancellationToken.Register()的调用。(不要因为被这段话弄糊涂了而感到难过。当我前一阵第一次读到它的时候,我也很困惑。

回调注册后令牌取消

通过调用CancellationTokenSource.Cancel()取消

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

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

在这两种情况下,像CancellationToken.Register()一样,可以使用正常的try...catch来捕获异常。

被CancellationTokenSource.CancelAfter()取消

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

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

在你的情况下,因为你使用CancelAfter(),拦截未处理的异常是你唯一的选择。try...catch不能工作

<标题>推荐

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

<标题>进一步阅读
  • CancellationTokenSource.Cancel() -讨论如何处理取消回调异常
  • 理解取消回调-我最近写的一篇关于这个主题的博客文章