繁体   English   中英

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

[英]Bank account kata with F# MailboxProcessor slow

我已经使用 F# MailboxProcessor对“经典”银行帐户 kata进行了编码,以确保线程安全。 但是,当我尝试将交易并行添加到帐户时,速度非常慢非常快:10 个并行调用响应(2 毫秒),20 个不响应(9 秒)! (查看最后一个测试Account can be updated from multiple threads

既然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

您的问题是您的代码没有一直使用 Async 。

您的帐户 class 具有方法OpenCloseBalanceTransaction ,您使用AsyncReplyChannel但您使用PostAndReply发送消息。 这意味着:您向 MailboxProcessor 发送一条带有回复通道的消息。 但是,此时,该方法会同步等待完成。

即使使用Async.Parallel和多个线程,也可能意味着很多线程会锁定它们自己。 如果您将所有方法更改为使用PostAndAsyncReply ,那么您的问题就会消失。

还有另外两个性能优化可以提高性能,但在您的示例中并不重要。

  1. 调用列表的长度是不好的。 要计算列表的长度,必须通过整个列表 go。 您只在 Transaction 中使用它来打印长度,但要考虑事务列表是否变长。 每当您添加交易时,您始终必须通过整个列表 go。 这将是您的交易列表的 O(N)。

  2. 调用 (List.sum) 也是如此。 每当您调用 Balance 时,您都必须计算当前余额。 也是 O(N)。

由于您有一个 MailboxProcessor,您还可以计算这两个值,而不是一次又一次地完全重新计算这些值。因此,它们成为 O(1) 操作。

最重要的是,我会更改OpenCloseTransaction消息以不返回任何内容,就像我认为的那样,它们返回任何内容都没有任何意义。 你的例子甚至让我对bool返回值的含义感到困惑。

Close消息中,您在将其设置为 false 之前返回state.Opened 为什么?

Open消息中,您返回否定的state.Opened 你以后如何使用它只是看起来不对。

如果bool背后有更多含义,请从中创建一个独特的区分联合,以描述它返回的目的。

您在整个代码中使用了一个option<Acount> ,我将其删除,因为我看不到它的任何用途。

无论如何,这是一个完整的示例,说明我将如何编写没有速度问题的代码。


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