.NET TPL CancellationToken内存泄漏

.NET TPL CancellationToken内存泄漏,.net,vb.net,memory-leaks,task-parallel-library,cancellation-token,.net,Vb.net,Memory Leaks,Task Parallel Library,Cancellation Token,我开发了一个库,它实现了工作项的生产者/消费者模式。工作将退出队列,并为每个退出队列的工作项启动一个单独的任务,该任务具有失败和成功的连续性 任务继续在工作项完成(或失败)其工作后重新排队 整个库共享一个中央CancellationTokenSource,在应用程序关闭时触发 我现在面临一个严重的内存泄漏。如果任务是以取消令牌作为参数创建的,则任务似乎会保留在内存中,直到触发取消源(并随后释放) 这可以在这个示例代码(VB.NET)中重现。主要任务是包装工作项的任务,而继续任务将处理重新调度 D

我开发了一个库,它实现了工作项的生产者/消费者模式。工作将退出队列,并为每个退出队列的工作项启动一个单独的任务,该任务具有失败和成功的连续性

任务继续在工作项完成(或失败)其工作后重新排队

整个库共享一个中央
CancellationTokenSource
,在应用程序关闭时触发

我现在面临一个严重的内存泄漏。如果任务是以取消令牌作为参数创建的,则任务似乎会保留在内存中,直到触发取消源(并随后释放)

这可以在这个示例代码(VB.NET)中重现。主要任务是包装工作项的任务,而继续任务将处理重新调度

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

我将不得不检查在取消的情况下这是如何执行的,但这似乎做到了。我几乎怀疑.NET框架中有一个bug。具有互斥条件的任务取消并非罕见。

我能够通过移动这两行来解决.net 4.0下的问题

Dim oCancellationTokenSource As New CancellationTokenSource
Dim oToken As CancellationToken = oCancellationTokenSource.Token
在第一个循环内

然后在循环结束时

oToken = Nothing
oCancellationTokenSource.Dispose()
我也搬家了

Interlocked.Decrement(nActiveTasks)

While nActiveTasks > 0
这是不准确的

下面是工作的代码

Imports System.Threading.Tasks
Imports System.Threading

Module Module1

Sub Main()
    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 oCancellationTokenSource As New CancellationTokenSource
        Dim oToken As CancellationToken = oCancellationTokenSource.Token
        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
            Dim outer As Integer = iteration
            Dim inner As Integer = i

            Interlocked.Increment(nActiveTasks)

            Dim oMainTask As New Task(Sub()
                                          ' perform some work
                                      End Sub, oToken, TaskCreationOptions.None)

            oMainTask.ContinueWith(Sub()
                                       Console.WriteLine("Failed " & outer & "." & inner)
                                       Interlocked.Decrement(nActiveTasks)
                                   End Sub, oToken, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default)


            oMainTask.ContinueWith(Sub()
                                       If inner Mod 250 = 0 Then Console.WriteLine("Success " & outer & "." & inner)
                                       Interlocked.Decrement(nActiveTasks)
                                   End Sub, oToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default)


            oMainTask.Start()
        Next

        Console.WriteLine("Memory after creating tasks: " & GC.GetTotalMemory(True).ToString("N0"))


        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

        oToken = Nothing
        oCancellationTokenSource.Dispose()

        Console.WriteLine("Memory after tasks finished: " & GC.GetTotalMemory(True).ToString("N0"))

    Next

    Console.WriteLine("Final Memory after finished: " & GC.GetTotalMemory(True).ToString("N0"))

    Console.Read()
End Sub

End Module
导入System.Threading.Tasks
导入系统线程
模块1
副标题()
Dim nActiveTasks作为整数=0
Dim LBASEMORY As Long=GC.GetTotalMemory(True)
对于迭代=0到100'执行此操作101次,以查看内存增加了多少
Dim oCancellationTokenSource作为新的CancellationTokenSource
Dim oToken As CancellationToken=oCancellationTokenSource.Token
Dim lMemory As Long=GC.GetTotalMemory(True)
Console.WriteLine(“迭代开始时的内存:&lMemory.ToString(“N0”))
控制台.WriteLine(“到基线:&(lMemory-lbasemory).ToString(“N0”))
对于i作为整数=0到1000'1001次迭代,以获得即时的、可测量的影响
将外部设置为整数=迭代
Dim INTERNAL As Integer=i
联锁增量(nActiveTasks)
将oMainTask作为新任务(子任务)
"做点工作,
End Sub、oToken、TaskCreationOptions.None)
oMainTask.ContinueWith(Sub()
Console.WriteLine(“失败”&外部&“&内部)
联锁减量(nActiveTasks)
End Sub、oToken、TaskContinuationOptions.OnlyOnFaulted、TaskScheduler.Default)
oMainTask.ContinueWith(Sub()
如果内部模块250=0,则Console.WriteLine(“成功”&外部&“&内部)
联锁减量(nActiveTasks)
End Sub、oToken、TaskContinuationOptions.OnlyOnRanToCompletion、TaskScheduler.Default)
oMainTask.Start()
下一个
Console.WriteLine(“创建任务后的内存:&GC.GetTotalMemory(True).ToString(“N0”))
Dim previousActive As Integer=nActiveTasks
而nActiveTasks>0
如果以前的活动nActiveTasks,则
Console.WriteLine(“活动:&nActiveTasks”)
线程。睡眠(500)
previousActive=nActiveTasks
如果结束
结束时
奥托肯=什么都没有
oCancellationTokenSource.Dispose()
Console.WriteLine(“任务完成后的内存:&GC.GetTotalMemory(True).ToString(“N0”))
下一个
Console.WriteLine(“完成后的最终内存:&GC.GetTotalMemory(True).ToString(“N0”))
Console.Read()
端接头
端模块

一些观察结果

  • 只有在存在未运行的任务“分支”的情况下,才可能出现潜在泄漏。在您的示例中,如果您注释掉了aulted
  • 任务的
    ,那么泄漏对我来说就消失了。如果您将代码更新为使
    oMainTask
    出现故障,从而使
    oFaulted
    任务运行而
    OSACCESS
    任务不运行,则注释掉
    OSACCESS
    可防止泄漏
  • 可能没有帮助,但如果在所有任务运行后调用
    oCancellationTokenSource.Cancel()
    ,内存将释放。Dispose没有帮助,也没有将取消源与任务一起处理的任何组合
  • 我看了一下4.5.2(有没有一种方法可以查看早期的框架?)我知道它不一定是相同的,但它有助于了解正在发生的事情的类型。基本上,当您将取消令牌传递给任务时,任务会向取消令牌的取消源注册自身。因此,取消源保存对所有任务的引用。我还不清楚为什么你的方案会泄露。如果我发现了什么,我会在有机会深入查看后更新
  • 解决方法

    将分支逻辑移动到始终运行的延续

    Dim continuation As Task =
        oMainTask.ContinueWith(
            Sub(antecendent)
                If antecendent.Status = TaskStatus.Faulted Then
                    'Handle errors
                ElseIf antecendent.Status = TaskStatus.RanToCompletion Then
                    'Do something else
                End If
            End Sub,
            oToken,
            TaskContinuationOptions.None,
            TaskScheduler.Default)
    

    无论如何,这很有可能比其他方法轻。在这两种情况下,始终会运行一个延续,但使用此代码只能创建一个延续任务,而不是2个。

    您是否可以在没有联锁的情况下尝试一下?在本例中,联锁仅用于同步-我希望在测量内存之前等待所有任务启动。移除它不会改变任何事情。你在哪里等他们?@i3arnon我哪里都不等。这些任务执行后台工作,但不会产生可处理的结果(例如:处理数据库或文件、数据转换等),然后我将返回。你需要联锁做什么?这将在每次迭代中取消取消源。当然你不会有任何内存泄漏。如果我取消
    Dim continuation As Task =
        oMainTask.ContinueWith(
            Sub(antecendent)
                If antecendent.Status = TaskStatus.Faulted Then
                    'Handle errors
                ElseIf antecendent.Status = TaskStatus.RanToCompletion Then
                    'Do something else
                End If
            End Sub,
            oToken,
            TaskContinuationOptions.None,
            TaskScheduler.Default)