Pergunta

I am using async I/O to communicate with an HID device, and I would like to throw a catchable exception when there is a timeout. I've got the following read method:

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;
        }
    }
}

The exception thrown from the Token's callback is not caught by any try/catch blocks and I'm not sure why. I assumed it would be thrown at the await, but it is not. Is there a way to catch this exception (or make it catchable by the caller of Read())?

EDIT: So I re-read the doc at msdn, and it says "Any exception the delegate generates will be propagated out of this method call."

I'm not sure what it means by "propagated out of this method call", because even if I move the .Register() call into the try block the exception is still not caught.

Foi útil?

Solução 2

EDIT: So I re-read the doc at msdn, and it says "Any exception the delegate generates will be propagated out of this method call."

I'm not sure what it means by "propagated out of this method call", because even if I move the .Register() call into the try block the exception is still not caught.

What this means is that the caller of your cancellation callback (the code inside .NET Runtime) won't make an attempt to catch any exceptions you may throw there, so they will be propagated outside your callback, on whatever stack frame and synchronization context the callback was invoked on. This may crash the application, so you really should handle all non-fatal exceptions inside your callback. Think of it as of an event handler. After all, there may be multiple callbacks registered with ct.Register(), and each might throw. Which exception should have been propagated then?

So, such exception will not be captured and propagated into the "client" side of the token (i.e., to the code which calls CancellationToken.ThrowIfCancellationRequested).

Here's an alternative approach to throw TimeoutException, if you need to differentiate between user cancellation (e.g., a "Stop" button) and a timeout:

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;
        }
    }
}

Outras dicas

I personally prefer to wrap the Cancellation logic into it's own method.

For example, given an extension method like:

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;
}

You can simplify your method down to:

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;
        }
    }
}

In this case, since your only goal is to perform a timeout, you can make this even simpler:

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
}

Then your method can be:

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;
    }
}

Exception handling for callbacks registered with CancellationToken.Register() is complex. :-)

Token Cancelled Before Callback Registration

If the cancellation token is cancelled before the cancellation callback is registered, the callback will be executed synchronously by CancellationToken.Register(). If the callback raises an exception, that exception will be propagated from Register() and so can be caught using a try...catch around it.

This propagation is what the statement you quoted refers to. For context, here's the full paragraph that that quote comes from.

If this token is already in the canceled state, the delegate will be run immediately and synchronously. Any exception the delegate generates will be propagated out of this method call.

"This method call" refers to the call to CancellationToken.Register(). (Don't feel bad about being confused by this paragraph. When I first read it a while back, I was puzzled, too.)

Token Cancelled After Callback Registration

Cancelled by Calling CancellationTokenSource.Cancel()

When the token is cancelled by calling this method, cancellation callbacks are executed synchronously by it. Depending on the overload of Cancel() that's used, either:

  • All cancellation callbacks will be run. Any exceptions raised will be combined into an AggregateException that is propagated out of Cancel().
  • All cancellation callbacks will be run unless and until one throws an exception. If a callback throws an exception, that exception will be propagated out of Cancel() (not wrapped in an AggregateException) and any unexecuted cancellation callbacks will be skipped.

In either case, like CancellationToken.Register(), a normal try...catch can be used to catch the exception.

Cancelled by CancellationTokenSource.CancelAfter()

This method starts a countdown timer then returns. When the timer reaches zero, the timer causes the cancellation process to run in the background.

Since CancelAfter() doesn't actually run the cancellation process, cancellation callback exceptions aren't propagated out of it. If you want to observer them, you'll need to revert to using some means of intercepting unhandled exceptions.

In your situation, since you're using CancelAfter(), intercepting the unhandled exception is your only option. try...catch won't work.

Recommendation

To avoid these complexities, when possible don't allow cancellation callbacks to throw exceptions.

Further Reading

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top