【问题标题】:F# MailboxProcessor limit parallelismF# MailboxProcessor 限制并行度
【发布时间】:2018-10-12 15:11:55
【问题描述】:

我是 F# 的新手,正在尝试使用 MailboxProcessor 来确保状态更改是独立完成的。

简而言之,我将操作(描述状态更改的不可变对象)发布到 MailboxProcessor,在递归函数中我读取消息并生成新状态(即在下面的示例中将项目添加到集合中)并将其发送状态到下一次递归。

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

预期输出是:

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

我得到的是:

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

阅读 MailboxProcessor 的文档并在谷歌上搜索,我的结论是它是一个消息队列,由“单线程”处理,但看起来它们都是并行处理的。

我完全不在场了吗?

【问题讨论】:

  • 我根据我的怀疑发布了一个答案,但是如果您可以发布一个重现该问题的最小完整示例(例如,包括 ApplicationStateHandleAction 的 shell 版本),它将有助于确定问题的根本原因。
  • 更改了代码以包含问题的完全可执行外壳,认为它与启动多个代理有关,然后与现在与单个代理有关。 @亚伦
  • 我已经根据你的新代码示例更新了我的答案

标签: multithreading f# agent mailboxprocessor


【解决方案1】:

问题是您认为AppHolder.Processor 每次都是同一个对象,但实际上每次都是不同的 MailboxProcessor。我将您的 AppHolder 代码更改为以下内容:

[<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

我所做的唯一更改是简化 Console.WriteLine 调用以使用 printfn%A 来获取更多调试细节,并添加将在 MailboxProcessor 构建之前立即执行的单个 printfn "Starting..." 调用并开始了。我得到的输出是:

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

注意printfn "Starting..." 行已经执行了四次。

这吸引了很多 F# 新手:member 关键字定义了一个属性,而不是一个字段。每次评估属性时,都会重新评估该属性的主体。所以每次访问AppHolder.Processor,都会得到一个新的MailboxProcessor。详情请见https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/members/properties

您可能想要的是以下内容:

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

    static member HandleAction (action:obj) =
        processor.Post action

【讨论】:

  • 感谢您的回答。我最终改用模块,从而摆脱了 F# 代码中的静态类。更清洁的解决方案。
【解决方案2】:

我认为问题一定出在您的 HandleAction 实施中。我实现了以下内容,它产生了预期的输出。

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

编辑

看到更新的代码示例后,我相信正确的 F# 解决方案就是将 AppHolder 类型从类切换为模块。更新后的代码如下:

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"

这会输出与之前相同的结果:

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

【讨论】:

  • 切换到一个模块基本上是正确的答案。我在回答中展示了如何使用static let binding,因为这更接近 OP 所写的内容,因此更有可能帮助他/她。但是对于实际代码来说,使用模块的建议更好。
  • @rmunn:使用静态类(和扩展模块)来保持可变状态是一个滑坡,事后看来,总是最好使用可以在有限范围内实例化的常规类。跨度>
  • @scrwtp - 我一般同意。但是在这种特定情况下,您会认为 MailboxProcessor 是可变状态吗?因为这是 AppHolder 中唯一保存的东西,无论是模块还是静态类。
  • @rmunn:邮箱保护的ApplicationState 值在这种设置下构成全局可变状态。它允许与 OOP 单例相同的“远距离操作”场景。使用模块来保持状态比静态类更危险,我看到人们以一种他们不会做错任何事的心态来处理它,因为他们使用的是模块,因此是惯用的 F#。是的,我会在这里显式地实例化并传递邮箱。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-05-22
  • 2021-06-17
  • 1970-01-01
  • 1970-01-01
  • 2017-06-26
  • 2011-05-03
  • 1970-01-01
相关资源
最近更新 更多