.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)