I developed a library that implements a producer / consumer pattern for work items. The work is canceled, and a separate task with the continuation of failure and success is created for each processed work object.
Continuing tasks reloads the work item after its completion (or failure) of its operation.
The entire library has one central CancellationTokenSource , which is launched when the application terminates.
Now I am facing a serious memory leak. If tasks are created using the undo marker as a parameter, then the tasks seem to remain in memory until the undo source is started (and later deleted).
This can be reproduced in this code sample (VB.NET). The main task is the task that will wrap the work item, and the continuation tasks will handle the rescheduling.
Dim oCancellationTokenSource As New CancellationTokenSource Dim oToken As CancellationToken = oCancellationTokenSource.Token Dim nActiveTasks As Integer = 0 Dim lBaseMemory As Long = GC.GetTotalMemory(True) For iteration = 0 To 100 ' do this 101 times to see how much the memory increases Dim lMemory As Long = GC.GetTotalMemory(True) Console.WriteLine("Memory at iteration start: " & lMemory.ToString("N0")) Console.WriteLine(" to baseline: " & (lMemory - lBaseMemory).ToString("N0")) For i As Integer = 0 To 1000 ' 1001 iterations to get an immediate, measurable impact Interlocked.Increment(nActiveTasks) Dim outer As Integer = i Dim oMainTask As New Task(Sub() ' perform some work Interlocked.Decrement(nActiveTasks) End Sub, oToken) Dim inner As Integer = 1 Dim oFaulted As Task = oMainTask.ContinueWith(Sub() Console.WriteLine("Failed " & outer & "." & inner) ' if failed, do something with the work and re-queue it, if possible ' (imagine code for re-queueing - essentially just a synchronized list.add) ' Does not help: ' oMainTask.Dispose() End Sub, oToken, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default) ' if not using token, does not cause increase in memory: 'End Sub, TaskContinuationOptions.OnlyOnFaulted) ' Does not help: ' oFaulted.ContinueWith(Sub() ' oFaulted.Dispose() ' End Sub, TaskContinuationOptions.NotOnFaulted) Dim oSucceeded As Task = oMainTask.ContinueWith(Sub() ' success ' re-queue for next iteration ' (imagine code for re-queueing - essentially just a synchronized list.add) ' Does not help: ' oMainTask.Dispose() End Sub, oToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default) ' if not using token, does not cause increase in memory: 'End Sub, TaskContinuationOptions.OnlyOnRanToCompletion) ' Does not help: ' oSucceeded.ContinueWith(Sub() ' oSucceeded.Dispose() ' End Sub, TaskContinuationOptions.NotOnFaulted) ' This does not help either and makes processing much slower due to the thrown exception (at least one of these tasks is cancelled) 'Dim oDisposeTask As New Task(Sub() ' Try ' Task.WaitAll({oMainTask, oFaulted, oSucceeded, oFaultedFaulted, oSuccededFaulted}) ' Catch ex As Exception ' End Try ' oMainTask.Dispose() ' oFaulted.Dispose() ' oSucceeded.Dispose() ' End Sub) oMainTask.Start() ' oDisposeTask.Start() Next Console.WriteLine("Memory after creating tasks: " & GC.GetTotalMemory(True).ToString("N0")) ' Wait until all main tasks are finished (may not mean that continuations finished) Dim previousActive As Integer = nActiveTasks While nActiveTasks > 0 If previousActive <> nActiveTasks Then Console.WriteLine("Active: " & nActiveTasks) Thread.Sleep(500) previousActive = nActiveTasks End If End While Console.WriteLine("Memory after tasks finished: " & GC.GetTotalMemory(True).ToString("N0")) Next
I measured memory usage with ANTI Memory Profiler and saw a significant increase in System.Threading.ExecutionContext, which returns to the continuation of the task and CancellationCallbackInfo .
As you can see, I already tried to get rid of tasks that use the cancellation token, but this does not seem to have any effect.
Edit
I am using .NET 4.0
Update
Even with a simple connection of the main task with continued failure, the memory usage is constantly increasing. The continuation of the task, apparently, hinders the deregistration of the undo marker.
So, if a task is chained with a continuation that fails (due to TaskContinuationOptions ), then there seems to be a memory leak. If there is only one continuation that is running, then I did not notice a memory leak.
Bypass
As a workaround, I can make one continuation without TaskContinuationOptions and handle the state of the parent task:
oMainTask.ContinueWith(Sub(t) If t.IsCanceled Then ' ignore ElseIf t.IsCompleted Then ' reschedule ElseIf t.IsFaulted Then ' error handling End If End Sub)
I will need to check how this happens in case of cancellation, but this seems to do the trick. I almost suspect a bug in the .NET Framework. Canceling jobs with mutually exclusive terms is not something that can be so rare.