Есть ли у Async.StartChild утечка памяти? - PullRequest
16 голосов
/ 28 января 2012

Когда я запускаю следующий тест (построенный с F # 2.0), я получаю OutOfMemoryException. Требуется около 5 минут, чтобы достичь исключения в моей системе (i7-920 6 ГБ ОЗУ, если он работал как процесс x86), но в любом случае мы можем видеть, как увеличивается память в диспетчере задач.

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

Я не вижу ничего плохого в этом коде и не вижу причин для увеличения памяти. Я сделал альтернативную реализацию, чтобы убедиться, что мои аргументы верны:

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

Для этого теста он работает хорошо без значительного потребления памяти. К сожалению, я не очень опытен в F # и у меня есть сомнения, если я пропущу некоторые вещи. В случае, если это ошибка, как я могу сообщить об этом команде F #?

1 Ответ

16 голосов
/ 28 января 2012

Я думаю, что вы правы - похоже, что при реализации StartChild.

произошла утечка памяти.

Я немного профилировал (следуя фантастическому учебнику Дэйва Томаса ) и F # релиз с открытым исходным кодом , и я думаю, что даже знаю, как это исправить. Если вы посмотрите на реализацию StartChild, она регистрирует обработчик с текущим токеном отмены рабочего процесса:

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

Объекты, которые остаются живыми в куче, являются экземплярами этой зарегистрированной функции. Их можно было бы отменить, позвонив по номеру _reg.Dispose(), но этого никогда не происходит в исходном коде F #. Я попытался добавить _reg.Dispose() к функциям, которые вызываются после завершения асинхронной работы:

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

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

Я отправлю сообщение об ошибке команде F # со ссылкой на ваш вопрос. Если вы найдете что-то еще, вы можете связаться с ними, отправив отчеты об ошибках на fsbugs в microsoft точка com.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...