The Auto-cancelling CancellationToken that Doesn’t

By Grauenwolf

The obvious way to create a CancellationToken that automatically cancels is to just create a CancellationTokenSource with a delay. Here’s an example:

using (var cs = new CancellationTokenSource(timeout)) {    var ct = cs.Token;

    await list.WhenAll(ct);
}

This works fine so long as you are able to hold onto a reference to the CancellationTokenSource. But what if you wanted to run something in the background?

using (var cs = new CancellationTokenSource(timeout)){    var ct = cs.Token;

    Task.Run(SomethingLongRunning, ct);
}

Now we’ve got a problem. The CancellationTokenSource is going to die as soon as it reaches the end of this block. If the timeout hasn’t been reached yet… well too bad. The internal timer callback is going to be ignored and your token will never cancel.

Now you could remove the `using` block, but then your CancellationTokenSource is never going to be disposed and you’re putting pressure on the finalizer to clear up the mess. (Specifically, the SafeHandle used by the ManualResetEvent used by the CancellationTokenSource.) What’s worse, some people report memory leaks from not disposing the CancellationTokenSource.

Proposed Solution

Rather than using the internal timer for CancellationTokenSource, we can create our own external timer. And this timer can handle the cleanup duties.

public static CancellationToken AutoCancelingToken(TimeSpan delay) {     if (delay.TotalMilliseconds < 0)

         throw new ArgumentOutOfRangeException(nameof(delay), delay, $”{nameof(delay)} cannot be less than 0″);

    var cts = new CancellationTokenSource();          var t = new System.Timers.Timer(delay.TotalMilliseconds);     t.AutoReset = false;     t.Elapsed += (source, e) =>     {         cts.Cancel();         cts.Dispose();         t.Dispose();     };     t.Start();     return cts.Token;

}

Note that we’re explicitly using a System.Timers.Timer instead of a System.Threading.Timer. This is important because a threading timer can be garbage collected unexpectedly.

Proposed Solution 2

So it turns out there’s a little trick on CancellationToken that I wasn’t aware of. You can register a callback for when the token is canceled. What can we do with that? Cancel the original CancellationTokenSource.

public static CancellationToken AutoCancelingToken(TimeSpan delay) {     if (delay.TotalMilliseconds < 0)

         throw new ArgumentOutOfRangeException(nameof(delay), delay, $”{nameof(delay)} cannot be less than 0″);

    var cts = new CancellationTokenSource(delay);     var result = cts.Token;     result.Register(() => cts.Dispose());     return result;

}

AutoCancelingToken will be included in Tortuga Anchor 3.1. (Or you can just steal the code above.)