11

Consider a scenario where you have some asynchronous work to be done and you can run it in a fire and forget mode. This asynchronous work is able to listen for cancellation and so you pass it a cancellation token in order to being able to cancel it.

At a given moment in time we can decide to request the cancellation of the ongoing activity, by using the cancellation token source object from which we have taken the cancellation token.

Because cancellation token source implements IDisposable, we should call its Dispose method as far as we are done with it. The point of this question is determining exactly when you are done with a given cancellation token source.

Let's suppose that you decide to cancel the ongoing work by calling the Cancel method on the cancellation token source: is it necessary to wait for the completion of the ongoing operation before calling Dispose ?

Put in other words, should I do this way:

class Program 
{
  static void Main(string[] args) 
  {
    var cts = new CancellationTokenSource();
    var token = cts.Token;

    DoSomeAsyncWork(token); // starts the asynchronous work in a fire and forget manner

    // do some other stuff here 

    cts.Cancel();
    cts.Dispose(); // I call Dispose immediately after cancelling without waiting for the completion of ongoing work listening to the cancellation requests via the token

    // do some other stuff here not involving the cancellation token source because it's disposed
  }

  async static Task DoSomeAsyncWork(CancellationToken token) 
  {
     await Task.Delay(5000, token).ConfigureAwait(false);
  }
}

or this way:

class Program 
{
  static async Task Main(string[] args) 
  {
    var cts = new CancellationTokenSource();
    var token = cts.Token;

    var task = DoSomeAsyncWork(token); // starts the asynchronous work in a fire and forget manner

    // do some other stuff here 

    cts.Cancel();

    try 
    {
      await task.ConfigureAwait(false);
    }
    catch(OperationCanceledException) 
    {
      // this exception is raised by design by the cancellation
    }
    catch (Exception) 
    {
      // an error has occurred in the asynchronous work before cancellation was requested
    }

    cts.Dispose(); // I call Dispose only when I'm sure that the ongoing work has completed

    // do some other stuff here not involving the cancellation token source because it's disposed
  }

  async static Task DoSomeAsyncWork(CancellationToken token) 
  {
     await Task.Delay(5000, token).ConfigureAwait(false);
  }
}

Additional details: the code I'm referring to is written inside an ASP.NET core 2.2 web application, here I'm using a console application scenario just to simplify my example.

I've found similar questions on stackoverflow asking for the need to dispose of cancellation token sources objects. Some of the answers suggest that in some scenario disposing of this object is not really needed.

My approach to the whole IDisposable subject is that I always tend to adhere to the exposed contract of a class, put another way if an object claims to be disposable I prefer to always call Dispose when I'm done with it. I don't like the idea of guessing whether or not calling dispose is really required by depending on implementation details of the class that could change in a future release in an undocumented manner.

Enrico Massone
  • 6,464
  • 1
  • 28
  • 56

3 Answers3

10

To ensure that a CTS (CancellationTokenSource) associated with a fire-and-forget Task will be eventually disposed, you should attach a continuation to the task, and dispose the CTS from inside the continuation. This creates a problem though, because another thread could call the Cancel method while the object is in the midst of its disposal, and according to the documentation the Dispose method is not thread-safe:

All public and protected members of CancellationTokenSource are thread-safe and may be used concurrently from multiple threads, with the exception of Dispose(), which must only be used when all other operations on the CancellationTokenSource object have completed.

So calling Cancel and Dispose from two different threads concurrently without synchronization is not an option. This leaves only one option available: to add a layer of synchronization around all public members of the CTS class. This is not a happy option though, for several reasons:

  1. You must write the thread-safe wrapper class (write code)
  2. You must use it every time you start a cancelable fire-and-forget task (write more code)
  3. Incur the performance penalty of the synchronization
  4. Incur the performance penalty of the attached continuations
  5. Having to maintain a system that has become more complex and more bug-prone
  6. Having to cope with the philosophical question why the class was not designed to be thread-safe in the first place

So my recommendation is to do the alternative, which is simply to leave the CTS undisposed, only in these cases where you can't await the completion of its associated tasks. In other words if it's not possible to enclose the code that uses the CTS in a using statement, just let the garbage collector to do the reclaiming of the reserved resources. This means that you'll have to disobey this part of the documentation:

Always call Dispose before you release your last reference to the CancellationTokenSource. Otherwise, the resources it is using will not be freed until the garbage collector calls the CancellationTokenSource object's Finalize method.

...and this:

The CancellationTokenSource class implements the IDisposable interface. You should be sure to call the CancellationTokenSource.Dispose method when you have finished using the cancellation token source to free any unmanaged resources it holds.

If this makes you feel a bit dirty, you are not alone. You may feel better if you think that the Task class implements the IDisposable interface too, but disposing task instances is not required.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Great answer! just a comment, i read that creating linkedtokensources can potentially leak if not disposed - do you have any ideas on this? – sommmen Apr 22 '20 at 09:16
  • @sommmen yeap, I have read this too. My only idea about it is: if you have to use a linked `CancellationTokenSource` with fire and-forget tasks, you are in trouble! – Theodor Zoulias Apr 22 '20 at 09:20
  • thanks for the contribution and the great article linked – Enrico Massone Apr 22 '20 at 10:32
  • the idea of attaching a continuation and calling dispose from there is a good, but i need to also being able to call Cancel from the other threads. So, i will opt for the second option of my thread. I'll call Cancel, then I'll wait for the task completion and then I'll call Dispose. – Enrico Massone Apr 22 '20 at 10:34
  • @EnricoMassone you can do that if you can be sure that the `CancellationTokenSource` will be canceled eventually. This is not always the case though. Also by awaiting for the completion of the task, it means that the task is not fire-and-forget any more! – Theodor Zoulias Apr 22 '20 at 10:55
  • @TheodorZoulias it won't be fire and forget anymore, but it's fine because by using awit I'll be able to not block anything, so it's fine for me. Given my scenario, it is entirely possible that Cancel method will be called from one thread, while from another thread the Task continuation is running and happily calling the Dispose method on the CancellationTokenSource. Unfortunately Dispose it's not thread safe, so I cannot follow this way. – Enrico Massone Apr 22 '20 at 11:01
  • 1
    Leaving CTS undisposed can lead to memory leaks! Currently writing a highly concurrent application which i just discovered had issues with CTS memory leaks, it's a pretty bad implementation if you ask me if your rely on cancellation with unknown sources of the said cancellation from different threads. – Felix K. Aug 26 '21 at 20:58
  • @FelixK. FYI some time ago I discovered that the quite popular Rx library [systematically omits disposing of the CancellationTokenSources it creates](https://stackoverflow.com/questions/66382339/does-the-rx-library-omits-disposing-of-the-cancellationtokensources-it-creates). As time passes by, I am becoming more confused regarding the disposability of the `CancellationTokenSource` class. In the meantime I've also made a helper class ([`CancelableExecution`](https://stackoverflow.com/a/61681938/11178549)) for managing the disposal of this class, just to discover how complex it becomes. – Theodor Zoulias Aug 26 '21 at 21:49
  • 1
    @TheodorZoulias Yes this isn't Microsofts finest work,. I don't want to use something else than ´CancellationToken´ in a library which might become public but it's really a pain to work with in some scenarios. – Felix K. Aug 26 '21 at 22:15
3

The correct practice is second - you dispose of the CancellationTokenSource after you are sure the task is cancelled. CancellationToken relies on information from CancellationTokenSource to function properly. While the current implementation CancellationToken is written in such a way that is will still work even without throwing exceptions if the CTS it was created from is disposed, it may not behave properly or always as expected.

Nick
  • 4,787
  • 2
  • 18
  • 24
1

Like any IDisposable you dispose it when you're done with the resource. That's the hard rule of IDisposable and i haven't encountered a situation where that was not the case but i am of course open to learning ;).

In case of the CancellationTokenSource this means that you dispose the source when both the object itself and the Token property are no longer used. (I just had a source open for this statement but alas i got distracted and lost it somehow)

So you dispose when tasks are no longer using the CancellationToken. In your case, the second option, since you then are sure that no tasks use the token.

edit; adding to this, its good practice to also set any properties to null that implement disposable. in this case since you only have local variables this does not matter, but when you have the token source as a field or something, make sure to set the field to null so that there are no references to the token source.

sommmen
  • 6,570
  • 2
  • 30
  • 51