[英]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 具有方法
Open
、 Close
、 Balance
和Transaction
,您使用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.还有另外两个性能优化可以提高性能,但在您的示例中并不重要。
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)。
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.最重要的是,我会更改
Open
、 Close
和Transaction
消息以不返回任何内容,就像我认为的那样,它们返回任何内容都没有任何意义。 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.