2012-01-28 26 views
15

Po uruchomieniu poniższego testu (zbudowanego z F # 2.0) otrzymuję OutOfMemoryException. Osiągnięcie wyjątku w moim systemie zajmuje około 5 minut (i7-920 6 gb pamięci RAM, jeśli działał jako proces x86), ale w każdym razie możemy zobaczyć, jak pamięć rośnie w menedżerze zadań.Czy Async.StartChild ma wyciek pamięci?

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...." 

Nie widzę nic złego w tym kodzie, a nie widzę żadnych powodów, dla pamięci rośnie. Zrobiłem alternatywną implementację aby upewnić się, że moje argumenty są ważne:

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) 
      ) 
     } 

Do tego testu to działa dobrze bez zasadniczo między zużyciem pamięci. Niestety nie mam dużego doświadczenia w F # i mam wątpliwości, czy tęsknię za niektórymi rzeczami. Jeśli jest to błąd, jak mogę zgłosić to zespołowi F #?

Odpowiedz

15

Myślę, że masz rację - wydaje się, że istnieje przeciek pamięci w implementacji StartChild.

Zrobiłem trochę profilowania (po fantastic tutorial by Dave Thomas) i open-source F# release i myślę, że nawet wiem, jak to naprawić. Jeśli spojrzeć na realizację StartChild, to rejestruje obsługi z obecnym anulowania dowód workflow:

let _reg = ct.Register(
    (fun _ -> 
     match !ctsRef with 
     | null ->() 
     | otherwise -> otherwise.Cancel()), null) 

Obiekty, które pozostają żywe w stercie są instancjami tej zarejestrowanej funkcji. Mogą zostać wyrejestrowane przez wywołanie _reg.Dispose(), ale to nigdy nie dzieje się w kodzie źródłowym F #. Próbowałem dodanie _reg.Dispose() do funkcji, które uzyskać wywołana, gdy asynchroniczny uzupełnia:

(fun res -> _reg.Dispose(); ctsRef := null; resultCell.RegisterResult (Ok res, reuseThread=true)) 
(fun err -> _reg.Dispose(); ctsRef := null; resultCell.RegisterResult (Error err,reuseThread=true)) 
(fun err -> _reg.Dispose(); ctsRef := null; resultCell.RegisterResult (Canceled err,reuseThread=true)) 

... i na podstawie moich doświadczeń, to rozwiązuje problem. Tak więc, jeśli chcesz obejść ten problem, prawdopodobnie możesz skopiować cały wymagany kod z control.fs i dodać go jako poprawkę.

Wyślę raport o błędzie do zespołu F # z linkiem do twojego pytania. Jeśli znajdziesz coś innego, możesz skontaktować się z nimi, przesyłając raporty o błędach do fsbugs pod adresem microsoft dot com.

+0

Czy wiesz, dlaczego jest to konieczne? Dlaczego utworzono nowy "CTS"? Czy nie wystarczy użycie oryginalnego 'ct'? – svick

+0

@svick - Dobre pytanie. Myślę, że wewnętrzny token anulujący jest używany do obsługi limitu czasu, który można określić dla 'StartChild' (ten limit czasu nie powinien anulować obliczeń, które nazywają' StartChild', chyba że faktycznie czekasz na wynik później). –

+0

Nie pomyślałem o tym. Tak, to ma sens. – svick