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.
Puoi provare senza l'Interblocco? – i3arnon
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
Dove li stai aspettando? – i3arnon