7

我開發了一個實現工作項生產者/消費者模式的庫。工作被取消,每個出隊工作項目都會有一個單獨的任務,失敗和成功延續。.NET TPL CancellationToken內存泄漏

任務繼續在工作完成(或失敗)後重新排隊工作項目。

整個庫共享一箇中心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 

我測量與螞蟻內存分析器內存使用,並看到了System.Threading.ExecutionContext,這要追溯到任務的延續和CancellationCallbackInfo的大量增加。

正如你所看到的,我已經試圖處理使用取消標記的任務,但是這似乎沒有效果。

編輯

我使用.NET 4.0

更新

即使只是鏈接上失敗的延續的主要任務,內存使用持續上升。任務的繼續似乎阻止取消令牌註冊的註銷。

所以,如果一個任務與一個不能運行的延續鏈接(由於TaskContinuationOptions),那麼似乎有內存泄漏。如果只有一個延續,運行,那麼我沒有觀察到內存泄漏。

解決方法

作爲一種變通方法,我可以做一個單一的延續,沒有任何TaskContinuationOptions和處理有父任務的狀態:

oMainTask.ContinueWith(Sub(t) 
        If t.IsCanceled Then 
         ' ignore 
        ElseIf t.IsCompleted Then 
         ' reschedule 

        ElseIf t.IsFaulted Then 
         ' error handling 

        End If 
        End Sub) 

我得查如何執行在取消的情況下,但這似乎是伎倆。我幾乎懷疑.NET Framework中的一個錯誤。互斥條件下的任務取消不是這種情況下可能出現的情況。

+0

您可以嘗試沒有聯鎖嗎? – i3arnon

+0

在這個例子中,互鎖只存在於同步中 - 我想在測量存儲器之前等待所有的任務已經開始。刪除它不會改變任何東西。 – urbanhusky

+0

你在等什麼? – i3arnon

回答

0

我能夠解決.net 4下的問題。0通過移動這些2行

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 
+0

這將取消取消源迭代。當然,你不會有任何內存泄漏。如果我在所有迭代之後取消它,它也會清理乾淨。這種節拍有一箇中央取消來源的目的:) – urbanhusky

4

一些觀察

  1. 潛在的泄漏似乎只存在於哪裏有任務「分支」不運行的情況下,代碼。在你的例子中,如果你註釋掉oFaulted任務,泄漏對我來說就消失了。如果更新代碼以使oMainTask錯誤,以便oFaulted任務運行並且oSucceeded任務不運行,則註釋掉oSucceeded可防止泄漏。
  2. 也許沒有幫助,但是如果在所有任務都運行後調用oCancellationTokenSource.Cancel(),內存就會釋放。處置不起作用,也沒有任何處理取消源和任務的組合。
  3. 我看了一下http://referencesource.microsoft.com/這是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) 

這是一個很好的機會,因爲反正其他方法更輕。在這兩種情況下,一個延續總是運行,但通過此代碼,只會創建1個延續任務,而不是2個。

+0

確認後,審查我的帖子後,我注意到你用我做過的同樣的解決方法更新。我想我幾天前閱讀過這篇文章,但沒有機會發布= T。如果我沒有發現其他任何有用的東西,我想我會刪除,如果這不添加任何新的東西。 –