Cuando ejecuto la siguiente prueba (construida con F # 2.0) obtengo OutOfMemoryException. Se tardan unos 5 minutos en alcanzar la excepción en mi sistema (i7-920 6gb ram si se ejecutaba como un proceso x86), pero en cualquier caso podemos ver cómo crece la memoria en el administrador de tareas.¿Tiene Async.StartChild una pérdida de memoria?
module start_child_test
open System
open System.Diagnostics
open System.Threading
open System.Threading.Tasks
let cnt = ref 0
let sw = Stopwatch.StartNew()
Async.RunSynchronously(async{
while true do
let! x = Async.StartChild(async{
if (Interlocked.Increment(cnt) % 100000) = 0 then
if sw.ElapsedMilliseconds > 0L then
printfn "ops per sec = %d" (100000L*1000L/sw.ElapsedMilliseconds)
else
printfn "ops per sec = INF"
sw.Restart()
GC.Collect()
})
do! x
})
printfn "done...."
No veo nada de malo en este código, y no veo ningún motivo para que la memoria crezca. Realicé una implementación alternativa para asegurarme de que mis argumentos son válidos:
module start_child_fix
open System
open System.Collections
open System.Collections.Generic
open System.Threading
open System.Threading.Tasks
type IAsyncCallbacks<'T> = interface
abstract member OnSuccess: result:'T -> unit
abstract member OnError: error:Exception -> unit
abstract member OnCancel: error:OperationCanceledException -> unit
end
type internal AsyncResult<'T> =
| Succeeded of 'T
| Failed of Exception
| Canceled of OperationCanceledException
type internal AsyncGate<'T> =
| Completed of AsyncResult<'T>
| Subscribed of IAsyncCallbacks<'T>
| Started
| Notified
type Async with
static member StartChildEx (comp:Async<'TRes>) = async{
let! ct = Async.CancellationToken
let gate = ref AsyncGate.Started
let CompleteWith(result:AsyncResult<'T>, callbacks:IAsyncCallbacks<'T>) =
if Interlocked.Exchange(gate, Notified) <> Notified then
match result with
| Succeeded v -> callbacks.OnSuccess(v)
| Failed e -> callbacks.OnError(e)
| Canceled e -> callbacks.OnCancel(e)
let ProcessResults (result:AsyncResult<'TRes>) =
let t = Interlocked.CompareExchange<AsyncGate<'TRes>>(gate, AsyncGate.Completed(result), AsyncGate.Started)
match t with
| Subscribed callbacks ->
CompleteWith(result, callbacks)
| _ ->()
let Subscribe (success, error, cancel) =
let callbacks = {
new IAsyncCallbacks<'TRes> with
member this.OnSuccess v = success v
member this.OnError e = error e
member this.OnCancel e = cancel e
}
let t = Interlocked.CompareExchange<AsyncGate<'TRes>>(gate, AsyncGate.Subscribed(callbacks), AsyncGate.Started)
match t with
| AsyncGate.Completed result ->
CompleteWith(result, callbacks)
| _ ->()
Async.StartWithContinuations(
computation = comp,
continuation = (fun v -> ProcessResults(AsyncResult.Succeeded(v))),
exceptionContinuation = (fun e -> ProcessResults(AsyncResult.Failed(e))),
cancellationContinuation = (fun e -> ProcessResults(AsyncResult.Canceled(e))),
cancellationToken = ct
)
return Async.FromContinuations(fun (success, error, cancel) ->
Subscribe(success, error, cancel)
)
}
Para esta prueba funciona bien sin un consumo considerable de memoria. Desafortunadamente no tengo mucha experiencia en F # y tengo dudas si extraño algunas cosas. En caso de que sea un error, ¿cómo puedo informarlo al equipo de F #?
¿Sabes por qué es esto necesario? ¿Por qué se crea un nuevo 'CTS'? ¿No bastaría simplemente con usar 'ct' original? – svick
@svick - Buena pregunta. Creo que el token de cancelación interno se usa para manejar el tiempo de espera que se puede especificar para 'StartChild' (este tiempo de espera no debe cancelar el cálculo que llamó' StartChild', a menos que realmente espere el resultado más adelante). –
No pensé en eso. Sí, eso tiene sentido. – svick