Search code examples
c#asynchronouscancellationtokensource

CancellationToken leaking memory


I have a timer that starts 2 tasks every 2 seconds... I keep track of those taks in a simple list, (so I can wait for them to complete when I stop the app).

The tasks effectively go to the database, run a couple of updates and complete.
The tasks themselves never take more than a second to run

...
// global variables to keep track of the running tasks.
List<Task> _tasks = new List<Task>();
CancellationTokenSource _cts = new CancellationTokenSource();

// in the timer function
private void Timer(object sender, ElapsedEventArgs e)
{
  _tasks.Add( Foo1Async(_cts.Token) );
  _tasks.Add( Foo2Async(_cts.Token) );
  // remove the completed ones
  _tasks.RemoveAll(t => t.IsCompleted);
}
...

After a few minutes the memory goes from 40Mb to 130Mb, (and climbing) ...

If I replace only the code below and and nothing else

...
  _tasks.Add( Foo1Async( CancellationToken.None)) );
  _tasks.Add( Foo2Async( CancellationToken.None)) );
...

The memory remains at a steady 40Mb and never increases.

A couple of points

  • I don't cancel any tasks during my test, (they all run to completion).
  • I don't throw any exceptions during my test.
  • The database I use is Sqlite
  • During boths tests I do exactly the same work, the only difference is the CancellationToken used.
  • The memory never seems to be released, even if I wait a long time.
  • The timer is a System.Timers.Timer and I only re-start it once the timer event has been processed.
  • I do not us any other tokens, (or token source)
  • I do not register for any token cancellation.

If I take a memory snapshot the number of CancellationCallbackInfo seem to be the issue, but I have no idea where they come from and how to release them.

Looking at the .NET source code, the callbacks are used in CancellationTokenSource but I am not really sure how more are added to it, (or how to free them).

Any suggestions as to what could cause the memory leak when I use _cts.Token vs when I use CancellationToken.None


Solution

  • There is/was an issue in some of the .NET DBCommand code, the async functions register for cancellation, but would only dispose if there is an exception. As you noticed in most cases it would just grow the list.

    This issue was fixed sometime last year in .NET core v2.x, (not sure if it ever will be fixed in .NET standard, I am using 4.6.1 and it still is an issue).

    One way I worked around it was by writing my own wrapper around the async functions I needed.

    You could also call the non-async functions, (but you lose the 'cancel' functionality that the async functions give you).