简体   繁体   English

带有 F# 邮箱处理器慢的银行帐户卡塔

[英]Bank account kata with F# MailboxProcessor slow

I've coded the "classical" bank account kata with F# MailboxProcessor to be thread safe.我已经使用 F# MailboxProcessor对“经典”银行帐户 kata进行了编码,以确保线程安全。 But when I try to parallelize adding a transaction to an account, it's very slow very quick: 10 parallel calls are responsive (2ms), 20 not (9 seconds)!但是,当我尝试将交易并行添加到帐户时,速度非常慢非常快:10 个并行调用响应(2 毫秒),20 个不响应(9 秒)! (See last test Account can be updated from multiple threads beneath) (查看最后一个测试Account can be updated from multiple threads

Since MailboxProcessor supports 30 million messages per second (see theburningmonk's article ), where the problem comes from?既然MailboxProcessor支持每秒 3000 万条消息(见theburningmonk 的文章),那么问题出在哪里呢?

// -- Domain ----

type Message =
    | Open of AsyncReplyChannel<bool>
    | Close of AsyncReplyChannel<bool>
    | Balance of AsyncReplyChannel<decimal option>
    | Transaction of decimal * AsyncReplyChannel<bool>

type AccountState = { Opened: bool; Transactions: decimal list }

type Account() =
    let agent = MailboxProcessor<Message>.Start(fun inbox ->
        let rec loop (state: AccountState) =
            async {
                let! message = inbox.Receive()
                match message with
                | Close channel ->
                    channel.Reply state.Opened
                    return! loop { state with Opened = false }
                | Open channel ->
                    printfn $"Opening"
                    channel.Reply (not state.Opened)
                    return! loop { state with Opened = true }
                | Transaction (tran, channel) ->
                    printfn $"Adding transaction {tran}, nb = {state.Transactions.Length}"
                    channel.Reply true
                    return! loop { state with Transactions = tran :: state.Transactions }
                | Balance channel ->
                    let balance =
                        if state.Opened then
                            state.Transactions |> List.sum |> Some
                        else
                            None
                    balance |> channel.Reply
                    return! loop state
            }
        loop { Opened = false; Transactions = [] }
    )

    member _.Open () = agent.PostAndReply(Open)
    member _.Close () = agent.PostAndReply(Close)
    member _.Balance () = agent.PostAndReply(Balance)
    member _.Transaction (transaction: decimal) =
        agent.PostAndReply(fun channel -> Transaction (transaction, channel))

// -- API ----

let mkBankAccount = Account

let openAccount (account: Account) =
    match account.Open() with
    | true -> Some account
    | false -> None

let closeAccount (account: Account option) =
    account |> Option.bind (fun a ->
        match a.Close() with
        | true -> Some a
        | false -> None)

let updateBalance transaction (account: Account option) =
    account |> Option.bind (fun a ->
        match a.Transaction(transaction) with
        | true -> Some a
        | false -> None)

let getBalance (account: Account option) =
    account |> Option.bind (fun a -> a.Balance())
// -- Tests ----

let should_equal expected actual =
    if expected = actual then
        Ok expected
    else
        Error (expected, actual)

let should_not_equal expected actual =
    if expected <> actual then
        Ok expected
    else
        Error (expected, actual)

let ``Returns empty balance after opening`` =
    let account = mkBankAccount() |> openAccount
    getBalance account |> should_equal (Some 0.0m)

let ``Check basic balance`` =
    let account = mkBankAccount() |> openAccount
    let openingBalance = account |> getBalance
    let updatedBalance =
        account
        |> updateBalance 10.0m
        |> getBalance
    openingBalance |> should_equal (Some 0.0m),
    updatedBalance |> should_equal (Some 10.0m)

let ``Balance can increment or decrement`` =
    let account = mkBankAccount() |> openAccount
    let openingBalance = account |> getBalance
    let addedBalance =
        account
        |> updateBalance 10.0m
        |> getBalance
    let subtractedBalance =
        account
        |> updateBalance -15.0m
        |> getBalance
    openingBalance |> should_equal (Some 0.0m),
    addedBalance |> should_equal (Some 10.0m),
    subtractedBalance |> should_equal (Some -5.0m)

let ``Account can be closed`` =
    let account =
        mkBankAccount()
        |> openAccount
        |> closeAccount
    getBalance account |> should_equal None,
    account |> should_not_equal None

#time
let ``Account can be updated from multiple threads`` =
    let account =
        mkBankAccount()
        |> openAccount
    let updateAccountAsync =
        async {
            account
            |> updateBalance 1.0m
            |> ignore
        }
    let nb = 10 // 👈 10 is quick (2ms), 20 is so long (9s)
    updateAccountAsync
    |> List.replicate nb
    |> Async.Parallel
    |> Async.RunSynchronously
    |> ignore
    getBalance account |> should_equal (Some (decimal nb))
#time

Your problem is that your code don't uses Async all the way up.您的问题是您的代码没有一直使用 Async 。

Your Account class has the method Open , Close , Balance and Transaction and you use a AsyncReplyChannel but you use PostAndReply to send the message.您的帐户 class 具有方法OpenCloseBalanceTransaction ,您使用AsyncReplyChannel但您使用PostAndReply发送消息。 This means: You send a message to the MailboxProcessor with a channel to reply.这意味着:您向 MailboxProcessor 发送一条带有回复通道的消息。 But, at this point, the method waits Synchronously to finish.但是,此时,该方法会同步等待完成。

Even with Async.Parallel and multiple threads it can mean a lot of threads lock themsels.即使使用Async.Parallel和多个线程,也可能意味着很多线程会锁定它们自己。 If you change all your Methods to use PostAndAsyncReply then your problem goes away.如果您将所有方法更改为使用PostAndAsyncReply ,那么您的问题就会消失。

There are two other performance optimization that can speed up performance, but are not critical in your example.还有另外两个性能优化可以提高性能,但在您的示例中并不重要。

  1. Calling the Length of a list is bad.调用列表的长度是不好的。 To calculate the length of a list, you must go through the whole list.要计算列表的长度,必须通过整个列表 go。 You only use this in Transaction to print the length, but consider if the transaction list becomes longer.您只在 Transaction 中使用它来打印长度,但要考虑事务列表是否变长。 You alway must go through the whole list, whenever you add a transaction.每当您添加交易时,您始终必须通过整个列表 go。 This will be O(N) of your transaction list.这将是您的交易列表的 O(N)。

  2. The same goes for calling (List.sum).调用 (List.sum) 也是如此。 You have to calculate the current Balance whenever you call Balance.每当您调用 Balance 时,您都必须计算当前余额。 Also O(N).也是 O(N)。

As you have a MailboxProcessor, you also could calculate those two values instead of completly recalculating those values again and again.Thus, they become O(1) operations.由于您有一个 MailboxProcessor,您还可以计算这两个值,而不是一次又一次地完全重新计算这些值。因此,它们成为 O(1) 操作。

On top, i would change the Open , Close and Transaction messages to return nothing, as in my Opinion, it doesn't make sense that they return anything.最重要的是,我会更改OpenCloseTransaction消息以不返回任何内容,就像我认为的那样,它们返回任何内容都没有任何意义。 Your examples even makes me confused of what the bool return values even mean.你的例子甚至让我对bool返回值的含义感到困惑。

In the Close message you return state.Opened before you set it to false.Close消息中,您在将其设置为 false 之前返回state.Opened Why?为什么?

In the Open message you return the negated state.Opened .Open消息中,您返回否定的state.Opened How you use it later it just looks wrong.你以后如何使用它只是看起来不对。

If there is more meaning behind the bool please make a distinct Discriminated Union out of it, that describes the purpose of what it returns.如果bool背后有更多含义,请从中创建一个独特的区分联合,以描述它返回的目的。

You used an option<Acount> throughout your code, i removed it, as i don't see any purpose of it.您在整个代码中使用了一个option<Acount> ,我将其删除,因为我看不到它的任何用途。

Anyway, here is a full example, of how i would write your code that don't have the speed problems.无论如何,这是一个完整的示例,说明我将如何编写没有速度问题的代码。


type Message =
    | Open
    | Close
    | Balance     of AsyncReplyChannel<decimal option>
    | Transaction of decimal

type AccountState = {
    Opened:             bool
    Transactions:       decimal list
    TransactionsLength: int
    CurrentBalance:     decimal
}

type Account() =
    let agent = MailboxProcessor<Message>.Start(fun inbox ->
        let rec loop (state: AccountState) = async {
            match! inbox.Receive() with
            | Close ->
                printfn "Closing"
                return! loop { state with Opened = false }
            | Open ->
                printfn "Opening"
                return! loop { state with Opened = true }
            | Transaction tran ->
                let l = state.TransactionsLength + 1
                printfn $"Adding transaction {tran}, nb = {l}"

                if state.Opened then
                    return! loop {
                        state with
                            Transactions       = tran :: state.Transactions
                            TransactionsLength = l
                            CurrentBalance     = state.CurrentBalance + tran
                    }
                else
                    return! loop state
            | Balance channel ->
                if   state.Opened
                then channel.Reply (Some state.CurrentBalance)
                else channel.Reply  None
                return! loop state
        }

        let defaultAccount = {
            Opened             = false
            Transactions       = []
            TransactionsLength = 0
            CurrentBalance     = 0m
        }
        loop defaultAccount
    )

    member _.Open        ()          = agent.Post(Open)
    member _.Close       ()          = agent.Post(Close)
    member _.Balance     ()          = agent.PostAndAsyncReply(Balance)
    member _.Transaction transaction = agent.Post(Transaction transaction)

(* Test *)

let should_equal expected actual =
    if expected = actual then
        Ok expected
    else
        Error (expected, actual)

(* --- API --- *)

let mkBankAccount = Account

(* Opens the Account *)
let openAccount  (account: Account) =
    account.Open ()

(* Closes the Account *)
let closeAccount (account: Account) =
    account.Close ()

(* Updates Account *)
let updateBalance transaction (account: Account) =
    account.Transaction(transaction)

(* Gets the current Balance *)
let getBalance (account: Account) =
    account.Balance ()

#time
let ``Account can be updated from multiple threads`` =
    let account = mkBankAccount ()
    openAccount account

    let updateBalanceAsync = async {
        updateBalance 1.0m account
    }

    let nb = 50

    List.replicate nb updateBalanceAsync
    |> Async.Parallel
    |> Async.RunSynchronously
    |> ignore

    Async.RunSynchronously (async {
        let! balance = getBalance account
        printfn "Balance is %A should be (Some %f)" balance (1.0m * decimal nb)
    })
#time

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM