2012-01-28 59 views
15

当我运行以下测试(使用F#2.0构建)时,出现OutOfMemoryException。大约需要5分钟才能在我的系统上发现异常(如果它是以x86进程运行,则为i7-920 6gb ram),但无论如何,我们都可以看到任务管理器内存如何增长。Async.StartChild是否有内存泄漏?

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#没有多少经验,如果我错过了一些事情,我会怀疑。如果是bug,我该如何报告给F#团队?

回答

15

我认为你是对的 - 在执行StartChild时似乎有内存泄漏。

我做了一些分析(以下fantastic tutorial by Dave Thomas)和open-source F# release,我想我甚至知道如何解决这个问题。如果你看看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#团队。如果你发现别的东西,你可以发送错误报告给fsbugsmicrosoft dot com与他们联系。

+0

你知道为什么这甚至是必要的吗?为什么创建一个新的CTS?不会仅仅使用原来的'ct'就够了吗? – svick 2012-01-28 18:16:11

+0

@svick - 好问题。我认为内部取消标记用于处理可以为'StartChild'指定的超时(这个超时不应该取消调用'StartChild'的计算,除非您实际上稍后等待结果)。 – 2012-01-28 19:13:44

+0

我没有想到这一点。是的,这是有道理的。 – svick 2012-01-28 19:22:59