Duncan
Duncan

Reputation: 1213

Disposing CancellationTokenRegistrations

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

Upvotes: 3

Views: 1385

Answers (2)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149518

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

Upvotes: 1

Stephen Cleary
Stephen Cleary

Reputation: 456342

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

Upvotes: 2

Related Questions