Correct pattern to dispose of cancellation token source

◇◆丶佛笑我妖孽 提交于 2021-01-02 12:13:03

问题


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.


回答1:


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.




回答2:


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.




回答3:


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.



来源:https://stackoverflow.com/questions/61359443/correct-pattern-to-dispose-of-cancellation-token-source

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!