F# MailboxProcessor limit parallelism
Asked Answered
D

2

5

I'm new to F# and trying to experiment with the MailboxProcessor to ensure that state changes are done in isolation.

In short, I am posting actions (immutable objects describing state chanage) to the MailboxProcessor, in the recursive function I read the message and generate a new state (i.e. add an item to a collection in the example below) and send that state to the next recursion.

open System

type AppliationState =
    {
        Store : string list
    }
    static member Default = 
        {
            Store = List.empty
        }
    member this.HandleAction (action:obj) =
        match action with
        | :? string as a -> { this with Store = a :: this.Store }
        | _ -> this

type Agent<'T> = MailboxProcessor<'T>     

[<AbstractClass; Sealed>]
type AppHolder private () =
    static member private Processor = Agent.Start(fun inbox ->
        let rec loop (s : AppliationState) =
            async {
                let! action = inbox.Receive()
                let s' = s.HandleAction action
                Console.WriteLine("{s: " + s.Store.Length.ToString() + " s': " + s'.Store.Length.ToString())
                return! loop s'
                }
        loop AppliationState.Default)

    static member HandleAction (action:obj) =
        AppHolder.Processor.Post action

[<EntryPoint>]
let main argv =
    AppHolder.HandleAction "a"
    AppHolder.HandleAction "b"
    AppHolder.HandleAction "c"
    AppHolder.HandleAction "d"

    Console.ReadLine()
    0 // return an integer exit code

Expected output is:

s: 0 s': 1
s: 1 s': 2
s: 2 s': 3
s: 3 s': 4  

What I get is:

s: 0 s': 1
s: 0 s': 1
s: 0 s': 1
s: 0 s': 1

Reading the documentation for the MailboxProcessor and googling about it my conclusion is that it is a Queue of messages, processed by a 'single-thread', instead it looks like they are all processed in parallel.

Am I totally off the field here?

Disarm answered 2/5, 2018 at 13:26 Comment(3)
I posted an answer based upon my suspicions, but if you could post a minimum complete example that reproduces the problem (including, for example, a shell version of ApplicationState and HandleAction), it would assist with determining the root cause of your problem.Province
changed code to contain fully executable shell of the problem, think it has to do more with it starting multiple agents then it has to do with the single agent now. @AaronDisarm
I have updated my answer based on your new code sampleProvince
P
3

I think the issue must be in your implementation of HandleAction. I implemented the following, and it produces the expected output.

open System

type ApplicationState =
    {
        Items: int list
    }
    static member Default = {Items = []}
    member this.HandleAction x = {this with Items = x::this.Items}

type Message = Add of int

let Processor = MailboxProcessor<Message>.Start(fun inbox ->
    let rec loop (s : ApplicationState) =
        async {
            let! (Add action) = inbox.Receive()
            let s' = s.HandleAction action
            Console.WriteLine("s: " + s.Items.Length.ToString() + " s': " + s'.Items.Length.ToString())
            return! loop s'
        }
    loop ApplicationState.Default)

Processor.Post (Add 1)
Processor.Post (Add 2)
Processor.Post (Add 3)
Processor.Post (Add 4)


// OUTPUT
// s: 0 s': 1
// s: 1 s': 2
// s: 2 s': 3
// s: 3 s': 4

EDIT

After seeing the updated code sample, I believe the correct F# solution would just be to switch the AppHolder type from being a class to a module. The updated code would like this:

open System

type AppliationState =
    {
        Store : string list
    }
    static member Default = 
        {
            Store = List.empty
        }
    member this.HandleAction (action:obj) =
        match action with
        | :? string as a -> { this with Store = a :: this.Store }
        | _ -> this

type Agent<'T> = MailboxProcessor<'T>     

module AppHolder =
    let private processor = Agent.Start(fun inbox ->
        let rec loop (s : AppliationState) =
            async {
                let! action = inbox.Receive()
                let s' = s.HandleAction action
                Console.WriteLine("{s: " + s.Store.Length.ToString() + " s': " + s'.Store.Length.ToString())
                return! loop s'
            }
        loop AppliationState.Default)

    let handleAction (action:obj) =
        processor.Post action


AppHolder.handleAction "a"
AppHolder.handleAction "b"
AppHolder.handleAction "c"
AppHolder.handleAction "d"

This outputs the same result as before:

{s: 0 s': 1
{s: 1 s': 2
{s: 2 s': 3
{s: 3 s': 4
Province answered 2/5, 2018 at 14:1 Comment(4)
Switching to a module is basically the right answer. I showed how to use a static let binding in my answer because that's closer to what the OP wrote and is therefore more likely to help him/her. But the suggestion to use a module is the better one for the actual code.Lindeman
@rmunn: Using static classes (and by extension modules) for holding mutable state is a slippery slope, in hindsight it's always preferable to use a regular class that you can instantiate in a limited scope.Procession
@Procession - I agree in general. But in this specific case, would you consider a MailboxProcessor to be mutable state? Because that's the only thing being held in AppHolder here, whether it's a module or a static class.Lindeman
@rmunn: the ApplicationState value that the mailbox protects constitutes global mutable state with this kind of setup. It allows for the same "action at a distance" scenarios that you would have with OOP singletons. Using modules to hold state is an even bigger offender than a static class, I've seen people approach it with a mindset that they can do nothing wrong, because they're using modules therefore idiomatic F#. Yes, I'd instantiate and pass the mailbox around explicitly here.Procession
L
7

The issue is that you think AppHolder.Processor is going to be the same object each time, but it's actually a different MailboxProcessor each time. I changed your AppHolder code to be the following:

[<AbstractClass; Sealed>]
type AppHolder private () =
    static member private Processor =
        printfn "Starting..."
        Agent.Start(fun inbox ->
        let rec loop (s : AppliationState) =
            async {
                let! action = inbox.Receive()
                let s' = s.HandleAction action
                printfn "{s: %A s': %A}" s s'
                return! loop s'
                }
        loop AppliationState.Default)

    static member HandleAction (action:obj) =
        AppHolder.Processor.Post action

The only changes I made was to simplify that Console.WriteLine call to use printfn and %A to get more debugging detail, and to add a single printfn "Starting..." call that will be executed immediately before the MailboxProcessor is built and started. And the output I got was:

Starting...
Starting...
Starting...
Starting...
{s: {Store = [];} s': {Store = ["b"];}}
{s: {Store = [];} s': {Store = ["d"];}}
{s: {Store = [];} s': {Store = ["c"];}}
{s: {Store = [];} s': {Store = ["a"];}}

Notice that the printfn "Starting..." line has been executed four times.

This catches a lot of F# newbies: the member keyword defines a property, not a field. Each time you evaluate the property, the body of that property is evaluated afresh. So each time you access AppHolder.Processor, you get a new MailboxProcessor. See https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/members/properties for more details.

What you probably wanted was the following:

[<AbstractClass; Sealed>]
type AppHolder private () =
    static let processor =
        printfn "Starting..."
        Agent.Start(fun inbox ->
            // ...
        )

    static member HandleAction (action:obj) =
        processor.Post action
Lindeman answered 2/5, 2018 at 17:18 Comment(1)
Thanks for the answer. I ended up moving to a module instead and thus getting rid of the static class in F# code. A cleaner solution.Disarm
P
3

I think the issue must be in your implementation of HandleAction. I implemented the following, and it produces the expected output.

open System

type ApplicationState =
    {
        Items: int list
    }
    static member Default = {Items = []}
    member this.HandleAction x = {this with Items = x::this.Items}

type Message = Add of int

let Processor = MailboxProcessor<Message>.Start(fun inbox ->
    let rec loop (s : ApplicationState) =
        async {
            let! (Add action) = inbox.Receive()
            let s' = s.HandleAction action
            Console.WriteLine("s: " + s.Items.Length.ToString() + " s': " + s'.Items.Length.ToString())
            return! loop s'
        }
    loop ApplicationState.Default)

Processor.Post (Add 1)
Processor.Post (Add 2)
Processor.Post (Add 3)
Processor.Post (Add 4)


// OUTPUT
// s: 0 s': 1
// s: 1 s': 2
// s: 2 s': 3
// s: 3 s': 4

EDIT

After seeing the updated code sample, I believe the correct F# solution would just be to switch the AppHolder type from being a class to a module. The updated code would like this:

open System

type AppliationState =
    {
        Store : string list
    }
    static member Default = 
        {
            Store = List.empty
        }
    member this.HandleAction (action:obj) =
        match action with
        | :? string as a -> { this with Store = a :: this.Store }
        | _ -> this

type Agent<'T> = MailboxProcessor<'T>     

module AppHolder =
    let private processor = Agent.Start(fun inbox ->
        let rec loop (s : AppliationState) =
            async {
                let! action = inbox.Receive()
                let s' = s.HandleAction action
                Console.WriteLine("{s: " + s.Store.Length.ToString() + " s': " + s'.Store.Length.ToString())
                return! loop s'
            }
        loop AppliationState.Default)

    let handleAction (action:obj) =
        processor.Post action


AppHolder.handleAction "a"
AppHolder.handleAction "b"
AppHolder.handleAction "c"
AppHolder.handleAction "d"

This outputs the same result as before:

{s: 0 s': 1
{s: 1 s': 2
{s: 2 s': 3
{s: 3 s': 4
Province answered 2/5, 2018 at 14:1 Comment(4)
Switching to a module is basically the right answer. I showed how to use a static let binding in my answer because that's closer to what the OP wrote and is therefore more likely to help him/her. But the suggestion to use a module is the better one for the actual code.Lindeman
@rmunn: Using static classes (and by extension modules) for holding mutable state is a slippery slope, in hindsight it's always preferable to use a regular class that you can instantiate in a limited scope.Procession
@Procession - I agree in general. But in this specific case, would you consider a MailboxProcessor to be mutable state? Because that's the only thing being held in AppHolder here, whether it's a module or a static class.Lindeman
@rmunn: the ApplicationState value that the mailbox protects constitutes global mutable state with this kind of setup. It allows for the same "action at a distance" scenarios that you would have with OOP singletons. Using modules to hold state is an even bigger offender than a static class, I've seen people approach it with a mindset that they can do nothing wrong, because they're using modules therefore idiomatic F#. Yes, I'd instantiate and pass the mailbox around explicitly here.Procession

© 2022 - 2024 — McMap. All rights reserved.