Tuesday, 19 October 2010

Memory leaks in asynchronous workflows

During the development of a concurrent distributed application for a client, we repeated a common mistake of tail recursing in an asynchronous workflow using the do! construct. For example:

let agent execute =
   new MailboxProcessor<_>(fun inbox ->
      let rec loop() = async {
         let! msg = inbox.Receive()
         execute msg
         do! loop()
         }
      loop())

This is problematic because using do! in this context leaks stack frames every time the workflows recurses (which is typically every time a message is processed). Moreover, F# uses a trampoline to implement this stack so the consequence is not a stack overflow but, rather, a gradual memory leak.

So if your concurrent applications appear to be leaking a few kilobytes for each message they process, search for instances of do! in tail position and replace them with return! like this:

let agent execute =
   new MailboxProcessor<_>(fun inbox ->
      let rec loop() = async {
         let! msg = inbox.Receive()
         execute msg
         return! loop()
         }
      loop())

2 comments:

Tomas Petricek said...

I agree this can be confusing, but it makes sense if you think about what "do!" means. A possible solution would be to define "async" computation builder without the "Zero" member (you can do that as a simple wrapper using the existing one). Then you would have to end all blocks with either "return!" or "return". The disadvantage is that you'd have to write "return ()" instead of omitting "else" branches...

Flying Frog Consultancy Ltd. said...

@Tomas: Given the nature of this bug, I would certainly prefer to have to write the "return" by hand.

I wonder how this problem was solved in OCaml...