How do you catch the CancellationToken.Register callback exceptions?

I use async-I / O to communicate with the HID device, and I would like to throw a catching exception when there is a timeout. I have the following reading 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 raised by the Token callback does not fall into any try / catch blocks, and I'm not sure why. I suggested that this will be thrown on hold, but it is not. Is there a way to catch this exception (or make it available to the calling user Read ())?

EDIT: So I re-read the msdn document and it says: "Any exception that the delegate throws will be thrown from this method call."

I'm not sure what this means, “propagated from this method call,” because even if I translate the .Register () call into a try block, the exception is still not caught.

+8
c # task-parallel-library cancellationtokensource
source share
3 answers

EDIT: So I re-read the document in msdn and it says: "Any delegate exception is thrown from this method call.

I'm not sure what this means, “extending from this method call,” because even if I translate the .Register () call into a try block, an exception is still not detected.

This means that your cancel callback (the code inside the .NET Runtime) will not try to catch any exceptions that you can throw there, so they will propagate outside your callback, in any frame of the stack and the synchronization context to which callback was called. This can cause the application to crash, so you really should handle all non-fatal exceptions inside your callback. Think of it as an event handler. After all, there can be several callbacks registered with ct.Register() , and everyone can drop it. What exception should have been extended then?

Thus, such an exception will not be caught and propagated to the "client" side of the token (i.e. to the code that calls CancellationToken.ThrowIfCancellationRequested ).

Here's an alternative approach to throwing a TimeoutException if you need to distinguish between user cancellation (for example, the Stop button) and 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; } } } 
+8
source share

I personally prefer to wrap the undo logic in my own method.

For example, taking into account the extension method, for example:

 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 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 time out, you can make it even easier:

 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 could 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; } } 
+7
source share

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

Canceled token before registering callback

If the cancellation token is canceled before the cancellation callback is registered, the callback will be executed synchronously on the CancellationToken.Register() . If the callback throws an exception, that exception will propagate from Register() and therefore can be detected using try...catch around it.

This distribution is what the statement you are quoting refers to. For context, here is the full paragraph that this quote comes from.

If this token is already in a cancellation state, the delegate will start immediately and synchronously. Any delegate exception thrown will be propagated from this method call.

"This method call" refers to a call to CancellationToken.Register() . (Don't feel bad that this paragraph bothers me. When I first read it some time ago, I was also puzzled.)

Canceled token after registering callback

Canceled by a call to CancellationTokenSource.Cancel ()

When a token is canceled by calling this method, cancel callbacks are executed synchronously with it. Depending on the overload used by Cancel() either:

  • All cancel callbacks will be triggered. Any exceptions made will be thrown into an AggregateException that is thrown from Cancel() .
  • All cancellation callbacks will run until an exception is thrown. If the callback throws an exception, that exception will be thrown from Cancel() (not wrapped in an AggregateException ), and any failed cancel callbacks will be skipped.

In any case, for example, CancellationToken.Register() , a regular try...catch can be used to throw an exception.

Canceled CancellationTokenSource.CancelAfter ()

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

Since CancelAfter() does not actually start the cancellation process, cancellation callback exceptions do not propagate from it. If you want to review them, you need to return to using some methods of catching unhandled exceptions.

In your situation, since you are using CancelAfter() , CancelAfter() an unhandled exception is your only option. try...catch will not work.

Recommendation

To avoid these difficulties, whenever possible, do not allow cancel callbacks to make exceptions.

additional literature

0
source share

All Articles