0

On playing with disposing CancellationTokenSource objects and linked CTS to see how it affects memory, I found that disposing the CTS also releases the objects from a linked CTS which was created from the CancellationTokenSource.Token. Why is that?

Background

I was investigating a memory leak, caused by CancellationTokenSource not being disposed, similar as to what has been reported here. The memory dump of the application revealed a huge amount of CancellationTokenSource and related objects. The application (console, C#, .Net 4.7.2, Windows Server) runs several jobs from a queue. The class to which I could root many of the objects in memory, creates many CancellationTokenSource but does not dispose them.

Screenshot from WinDbg dump file analysis

Looking for info about CancellationTokenSource and memory leaks, I came across this really informative question here on SO: When to dispose CancellationTokenSource?.

For me, not (yet :-)) a tasks expert, there are some contradictory statements in it. The Microsoft docs are pretty clear about disposing of CancellationTokenSource (reference)

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.

Still, there seems to be cases where disposing can bring you in trouble, especially when the CTS gets cancelled while you await an operation. Also there is an answer quoting Stephen Toub:

It depends. In .NET 4, CTS.Dispose served two primary purposes. If the CancellationToken's WaitHandle had been accessed (thus lazily allocating it), Dispose will dispose of that handle. Additionally, if the CTS was created via the CreateLinkedTokenSource method, Dispose will unlink the CTS from the tokens it was linked to. In .NET 4.5, Dispose has an additional purpose, which is if the CTS uses a Timer under the covers (e.g. CancelAfter was called), the Timer will be Disposed.

It's very rare for CancellationToken.WaitHandle to be used, so cleaning up after it typically isn't a great reason to use Dispose. If, however, you're creating your CTS with CreateLinkedTokenSource, or if you're using the CTS' timer functionality, it can be more impactful to use Dispose.

Probably I just didn't fully understand his point, but this could be interpreted like "you can, but must not dispose".

Tests

To get more clarity for myself, if what I have found in the app can be the reason for the memory leak, I made up a test. The test code is simplified, but uses the same "pattern" as the application. Being able to compare results from different tests, the queue is filled with 200 jobs which are then processed by the worker. In the real app, jobs keep coming in and get processed in an infinite loop.

The full code can be found here: https://dotnetfiddle.net/8AMXgf

For every job, there are 3 tokens at play:

  1. "global" CancellationToken, to cancel the whole process when app is stopped (ct)
  2. job specific CancellationTokenSource, to cancel after 5 mins (Cts)
  3. linked CancellationTokenSource, links 1 + 2 (linkedCts)

The tests are:

  1. Dispose only linkedCts
  2. Dispose linkedCts and Cts
  3. Dispose only Cts

"Benchmark"

The "benchmark" I compared the results to, basically mirrors the current state of the real app, which is not disposing anything. The memory snapshots shown, were taken right after the 200 jobs have been processed.

Here is the code part which creates tasks and adds them to a process queue.

foreach (var job in jobsToRun)
{
    var runProcess = new ProcessDescriptor()
    {
        Descriptor = job.Value,
        Cts = new CancellationTokenSource(TimeSpan.FromMinutes(5))
    };

    // link cts with "global" token (ct)
    var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, runProcess.Cts.Token);

    runProcess.Task = RunJob1(job.Value, linkedCts.Token);

    _processQueue.Add(runProcess);
}

When the job is done, it's removed from the queue, but not disposed.

foreach (var process in _processQueue.ToList())
{
    if (process.Task.Status == TaskStatus.RanToCompletion)
    {
        // use dispose() for test 2 & test 3
        // process.CancellationTokenSource.Dispose();
        _processQueue.Remove(process);
    }
}

After 200 jobs have been processed. The heap looks like this. 200 * Cts and 200 * linkedCts = 400 CancellationTokenSource objects.

Benchmark statistic

Even after a forced GC.Collect the CancellationTokenSource objects still remain in memory.

enter image description here

Test 1: Dispose only linked CancellationTokenSource

This goes back to the comment from Stephen Toub (reference)

If, however, you're creating your CTS with CreateLinkedTokenSource, or if you're using the CTS' timer functionality, it can be more impactful to use Dispose.

For this I chose to pass linkedCts cts one level down, where it is disposed when the job is done. (It's probably not best practice to do so. But I can't await here, so usingortry... catch... finallyisn't an option. As an alternative.ContinueWith()` could probably be better. But I don't know. That would be another question...)

// pass on the CTS instead of the token (probably not best practice), which will be disposed when job is done - use this line for test 1 & test 2
runProcess.Task = RunJob2(job.Value, linkedCts);

The result shows, that half of the CancellationTokenSource objects have been released from memory. Still 200 objects remain.

enter image description here

Test 2: Dispose linked CancellationTokenSource and CancellationTokenSource

Here both Cts and linkedCts get disposed when a job is done. The result shows, that all CancellationTokenSource objects (besides the 2 global instances) have been released from memory.

enter image description here

Test 3:

This time, only the Cts gets disposed. The linkedCts, which is created from Cts and the global ct, is not being disposed. I'd expect the same result as with test 1 (half of the objects being released).

The result shows the exakt same picture as with test 2 where both cts and linked cts have been disposed. This confuses me.

enter image description here

Why does disposing of Cts only, show the same result as disposing Cts and linkedCts? To me it seems that disposing Cts frees resources of linkedCts to - even though linkedCts is also linked to ct which is still "alive"?

(Sorry for the lengthy post. I wanted to put all info in. Feel free to edit, if you feel it improves the question.)

Maddin
  • 298
  • 3
  • 11
  • Instead of studying is such a great detail what happens when you don't dispose the `CancellationTokenSource`s, why not go a step further and just dispose them? – Theodor Zoulias Nov 10 '22 at 23:23
  • @TheodorZoulias Sure. But dispose both `cts` and `linkedCts`? `cts` is easy. I've saved it and can dispose before removing from queue. But how to dispose the `linkedCts`? The process can't be awaited, so `using` isn't an option. With continuation? You've mentioned some drawbacks [here](https://stackoverflow.com/a/61361371/1638842). By passing it down? Or is it enough to only dispose the `cts` and forget the `linkedCts` and disobey the docs? I can try and see if the leak goes away. I am not sure. The topic is new to me. That's why I showed my findings and asked for advice to understand and act. – Maddin Nov 10 '22 at 23:44
  • It might not be easy, but it should be doable. If you manage to do it then your code will follow the official guidance, which is good, and also won't have to do all these experiments with the unpredictable outcome. If I was in your shoes I would try to dispose all of them, and only search for alternatives in case of desperation. – Theodor Zoulias Nov 11 '22 at 00:03

0 Answers0