Best use of F# Async maxDegreeOfParallelism

Viewed 209

I have been using F# Async workflow for awhile and really enjoying it. Recently, I'm working on a project involving many independent computation on separate Deedle Frame which I make use of Async.Parallel. And in those asyncs, many other asyncs are also computed in parallel fashion (nested Async?). My code is something like this

let processRoomAsync room = 
    async{
        let x = do_something room
        return x
    }

let processCompressorAsync compressor = 
    async{
        let rooms = get_rooms_from_compressor compressor
        let! y = 
            Array.map processRoomAsync rooms
            |> Async.Parallel
        return do_something_with_y y
    }

let processBuilding building = 
    let compressors = get_compressors_for_building building
    let processResult = 
        Array.map processCompressorAsync compressors
        |> Async.Parallel
        |> Async.RunSynchronously
    do_something_with_result processResult

The idea of the above code is that I have a building, e.g. a hotel, with many AC compressors, each compressor serves many rooms. I need to process all the rooms data that connect to one compressor in-order to model that compressor power. Then all compressors data is aggregated to give overall result of the building.

When the number of Async is small, the result is expected when compared with single async execution time.

However, when the number of async is more than 100, I notice significant degradation of computational time. I read some document about maxDegreeOfParallelism but I don't quite get which number I should use. Should it be the number of vCPU in my computer? If so, what's about those nested Async.Parallel? I have tried several values and the improvement is quite minimal.

I read somewhere that I should use MailboxProcessor but don't quite understand that. Also, some documentation says async is for I/O bound not for computation and this post seems to suggest to use Hopac but I wonder is it worth to spend time study about it?

Thanks and sorry for the long question.

1 Answers

Potential problems:

  • You are running more threads than your computer is capable of handling, so it's pausing one thread to get back to another and so on. This is where maxDegreeOfParallelism could help setting it to <= the number of logical processors you have. This is assuming all your computations are in memory. I don't think you should do nested Parallels, the goal is to keep your overall threads <= to the number of logical processors you have.

  • The other problem is your operations maybe running into contentions e.g. they're trying to use the same IO resources so they need to wait until they're freed. The more operations you add in parallel the more they need to wait as there's a limited throughput in the IO resource for example the database. Serialization of IO could help for example enlisting all IO operations in a queue e.g. Mailboxprocessor so there are no contentions. Ultimately the best way to improve the situation is to resolve the IO bottleneck.

Perhaps something like this could help:

  let yProcessor = MailboxProcessor.Start(fun inbox ->
        let rec messageLoop acc = async{

            let! msg = inbox.Receive()

            let result = do_something_with_y msg

            return! messageLoop acc::result
            }

        messageLoop ([])
        )


let processCompressorAsync compressor = 
   get_rooms_from_compressor compressor
   |>  Array.map do_something

let processBuilding building = 
    let compressors = get_compressors_for_building building
    let preComputation = 
        compressors
        |> Array.map processCompressorAsync
        |> Async.Parallel
        |> Async.RunSynchronously

    let processResult = 
        preComputation
        |> Array.iter (yProcessor.Post)
        |> Async.Parallel
        |> Async.RunSynchronously
Related