3

Contrived example, but suppose I have the following in an async method:

var cts = new CancellationTokenSource();
cts.CancelAfter(2000);
cts.Token.Register(Callback);
SomethingThatMightThrow();
await Task.Delay(10000, cts.Token);

This works as expected insofar as after a couple of seconds Callback is called. However, I want to dispose the registration after the Task.Delay, so suppose I make the following modification:

var cts = new CancellationTokenSource();
cts.CancelAfter(2000);
using (cts.Token.Register(Callback))
{
    SomethingThatMightThrow();
    await Task.Delay(10000, cts.Token);
}

In this case, Callback is not called, presumably because the exception from the await Task.Delay... causes the registration to be disposed before it gets invoked.

What's the best way of ensuring both that Callback gets called on cancellation and that the registration always gets disposed?

I did think of the following, but I'm not sure how robust it is:

var cts = new CancellationTokenSource();
cts.CancelAfter(2000);

var ctr = cts.Token.Register(Callback);
try
{
    SomethingThatMightThrow();
    await Task.Delay(10000, cts.Token);
}
finally
{
    if (!cts.Token.IsCancellationRequested)
        ctr.Dispose();
}
Duncan
  • 1,183
  • 2
  • 12
  • 22
  • 1
    It is not clear whether you *always* want/expect the `Callback` to be called or if there is some point within the code after which you would no longer want the callback to be called even if the CancelAfter time is up. If you always want the Callback to be called (even if the rest of your code is finished), just dispose of the `CancellationTokenRegistration` in the `Callback` – Matt Smith Aug 29 '14 at 12:12
  • 1
    Related: http://stackoverflow.com/questions/21367695/why-cancellationtokenregistration-exists-and-why-does-it-implement-idisposable – Matt Smith Aug 29 '14 at 12:17
  • 1
    @MattSmith - I wanted `Callback` to be called only if cancellation is requested during the `SomethingThatMightThrow(); await Task.Delay(10000, cts.Token);` block. – Duncan Aug 29 '14 at 12:31
  • Both Task.Delay and Token.Register are registering callbacks on the token. Task.Delay's callback gets called first (which causes the await to finish and the using statement to complete and the CancellationTokenRegistration to be disposed). Thus, by the time it gets to the next callback, it has already been unregistered. – Matt Smith Aug 29 '14 at 13:39
  • 1
    @MattSmith - yes, that's what I imagined was happening, which was the purpose my question really - given that that's the case and the second code snippet I provided therefore doesn't work (because `Callback` isn't invoked), what's the best alternative? – Duncan Aug 29 '14 at 13:57

2 Answers2

2

CancellationToken.Register is generally used to interop the new CancellationToken system with an older system that uses some other kind of notification for cancellation. It is not intended for use as a general-purpose "cancellation callback".

If you want to respond some way when an operation is canceled, then you just catch the appropriate exception:

using (var cts = new CancellationTokenSource())
{
  cts.CancelAfter(2000);
  SomethingThatMightThrow();
  try
  {
    await Task.Delay(10000, cts.Token);
  }
  catch (OperationCanceledException)
  {
    Callback();
  }
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    Thanks, that's helpful. My concern with this approach, however, would be that I'm now relying on the method I'm awaiting in the try block throwing an OperationCanceledException in a timely manner (`Task.Delay` behaves nicely, I'm sure, but in my actual code I'd be awaiting something else) - my thinking was that by registering a callback on the token instead I'd be guaranteeing that it would get invoked very shortly after the cancellation request. – Duncan Aug 29 '14 at 13:29
  • @Duncan: If a method takes a `CancellationToken`, it should throw an `OperationCanceledException`. – Stephen Cleary Aug 29 '14 at 14:39
  • 1
    it's not so much that the method might not throw the exception, as that it might not throw it as quickly as I'd like. I've noticed some of the Azure SDK async methods, for example, can take quite a while to respond to cancellation being signalled on the token... – Duncan Aug 29 '14 at 15:33
  • If you're sure the token *will* be canceled, then you can `Register` (without disposing) and use `Task.WhenAll` to return as soon as the cancellation goes out. – Stephen Cleary Aug 29 '14 at 15:44
  • @Duncan Another approach would be to create a `Task` that times out after the period you would like it to timeout, and use `Task.WhenAny` with the azure request and your timeout, and check if it was the timeout task or the azure sdk which finished first. – Yuval Itzchakov Aug 30 '14 at 08:41
1

it's not so much that the method might not throw the exception, as that it might not throw it as quickly as I'd like. I've noticed some of the Azure SDK async methods, for example, can take quite a while to respond to cancellation being signalled on the token

As per you comment, you can choose to create your own timer to explicitly specify when a method "is taking too long to run" according to your standards. For that you can use Task.WhenAny:

using (var cts = new CancellationTokenSource())
{
    try
    {
        var cancellationDelayTask = Task.Delay(2000, cts.Token);
        var taskThatMightThrow = SomethingThatMightThrowAsync(cts.Token);

        if ((await Task.WhenAny(taskThatMightThrow, cancellationDelayTask)) 
             == cancellationDelayTask)
        {
            // Task.Delay "timeout" finished first.
        }
    }
    catch (OperationCanceledException)
    {
        Callback();
    }
}
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321