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 

Я измерил использование памяти с МУРАВЬЯМИ Memory Profiler и увидел большое увеличение System.Threading.ExecutionContext, берущего начало продолжений задачи и CancellationCallbackInfo.

Как вы можете видеть, я уже пытался решить задачи, которые используют токен отмены, но это, похоже, не имеет никакого эффекта.

Редактировать

Я использую .NET 4.0

Update

Даже если только сцепление главную задачу с продолжением на провал, использование памяти постоянно растет. Продолжение задачи, по-видимому, препятствует снятию регистрации с регистрации маркера отмены.

Так что если задача связана цепью с продолжением, которое не работает (из-за 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() после выполнения всех задач, память освобождается. Dispose не помогает, ни любая комбинация Disposing источника отмены вместе с задачами.
  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) 

Есть хороший шанс, что это легче, чем другой подход. В обоих случаях всегда выполняется одно продолжение, но с этим кодом создается только одна задача продолжения вместо 2.

+0

Ack, сразу после рассмотрения моего сообщения, я заметил, что вы обновили тот же обходной путь, который я сделал. Кажется, я прочитал это пару дней назад и не получил возможность отправить = T. Если я не найду ничего полезного, я думаю, что я удалю, если это не добавит ничего нового. –

Смежные вопросы