7

Ho sviluppato una libreria che implementa un modello produttore/consumatore per gli articoli di lavoro. Il lavoro viene rimosso dalla coda e un task separato con continuazioni per errore e successo viene attivato per ogni elemento di lavoro dequalificato.Cancellazione TPL .NETDenuta di memoriaToken

Le continuazioni di attività ri-accodano l'elemento di lavoro dopo aver completato (o non riuscito) il suo lavoro.

L'intera libreria condivide uno CancellationTokenSource centrale, che viene attivato all'arresto dell'applicazione.

Ora devo affrontare una perdita di memoria importante. Se le attività vengono create con il token di annullamento come parametro, le attività sembrano rimanere in memoria fino a quando l'origine di annullamento non viene attivata (e successivamente eliminata).

Questo può essere riprodotto in questo codice di esempio (VB.NET). L'attività principale è l'attività che avvolgerebbe l'elemento di lavoro e le attività di continuazione gestiranno la riprogrammazione.

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 

ho misurato l'uso di memoria con le formiche Memory Profiler e ha visto un forte aumento del System.Threading.ExecutionContext, che risale a continuazioni attività e CancellationCallbackInfo.

Come potete vedere, ho già provato a disporre delle attività che utilizzano il token di cancellazione, ma questo sembra non avere alcun effetto.

Modifica

sto usando .NET 4.0

Aggiornamento

Anche quando solo il concatenamento compito principale con una continuazione in caso di fallimento, l'utilizzo della memoria aumenta continuamente. La continuazione dell'attività sembra impedire la cancellazione dalla registrazione del token di cancellazione.

Quindi se un'attività è concatenata con una continuazione, che non viene eseguita (a causa dello TaskContinuationOptions), sembra che ci sia una perdita di memoria. Se c'è solo una continuazione, che viene eseguita, allora non ho osservato una perdita di memoria.

Soluzione

Per aggirare il problema, posso fare un unico continuazione senza alcun TaskContinuationOptions e gestire lo stato di l'attività principale c'è:

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

        ElseIf t.IsFaulted Then 
         ' error handling 

        End If 
        End Sub) 

dovrò verificare come questo esegue in caso di cancellazione, ma questo sembra fare il trucco. Quasi ho il sospetto di un bug in .NET Framework. Cancellazioni di incarichi con condizioni esclusive reciproche non sono qualcosa che potrebbe essere così raro.

+0

Puoi provare senza l'Interblocco? – i3arnon

+0

In questo esempio esiste l'interblocco per la sincronizzazione: voglio attendere l'avvio di tutte le attività prima di misurare la memoria. Rimozione non cambia nulla. – urbanhusky

+0

Dove li stai aspettando? – i3arnon

risposta

0

Sono stato in grado di risolvere il problema in .net 4.0 spostando queste 2 righe

Dim oCancellationTokenSource As New CancellationTokenSource 
Dim oToken As CancellationToken = oCancellationTokenSource.Token 

all'interno del primo occhiello

poi alla fine di tale ciclo

oToken = Nothing 
oCancellationTokenSource.Dispose() 

anche Ho spostato il

Interlocked.Decrement(nActiveTasks) 

all'interno di ogni " finale "attività dal

While nActiveTasks > 0 

non sarebbe accurato.

qui il codice che funziona

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

Questo annullerebbe la fonte di cancellazione in ogni iterazione. Ovviamente non si hanno perdite di memoria. Se annullo dopo tutte le iterazioni, pulisce anche bene. Tipo di battute lo scopo di avere una fonte di cancellazione centrale :) – urbanhusky

4

Alcune osservazioni

  1. Il potenziale di perdita sembra presente solo nel caso in cui v'è una "succursale" compito che non viene eseguito. Nell'esempio, se si commenta l'attività oFaulted, la perdita viene persa per me. Se si aggiorna il codice per l'errore oMainTask, in modo che l'attività oFaulted venga eseguita e l'attività oSucceeded non venga eseguita, la creazione di commenti oSucceeded impedisce la perdita.
  2. Forse non utile, ma se chiami oCancellationTokenSource.Cancel() dopo che tutte le attività sono state eseguite, la memoria si libera. Smaltire non aiuta, né alcuna combinazione di Smaltimento della fonte di cancellazione insieme alle attività.
  3. Ho dato un'occhiata a http://referencesource.microsoft.com/ che è 4.5.2 (C'è un modo per visualizzare i primi framework?) So che non è necessariamente lo stesso, ma è utile sapere quali tipi di cose stanno accadendo. Fondamentalmente quando si passa un token di cancellazione a un'attività, l'attività si registra con la fonte di cancellazione del token di annullamento. Quindi la fonte di cancellazione contiene i riferimenti a tutti i tuoi compiti. Non sono ancora chiaro sul perché il tuo scenario sembra fuoriuscire. Aggiornerò dopo che avrò avuto la possibilità di guardare più in profondità, se trovo qualcosa.

Soluzione

Spostare la logica ramificazione a una continuazione che funziona sempre.

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) 

C'è una buona possibilità che questo sia più leggero rispetto all'altro approccio comunque. In entrambi i casi viene sempre eseguita una continuazione, ma con questo codice viene creato solo 1 task di continuazione anziché 2.

+0

Ack, subito dopo aver esaminato il mio post, ho notato che hai aggiornato con la stessa soluzione che ho fatto. Penso di aver letto questo un paio di giorni fa e non ho avuto la possibilità di postare = T. Se non trovo nient'altro utile, suppongo che cancellerò se non aggiungo nulla di nuovo. –