我一直在尝试设计一个系统,允许大量并发用户同时在内存中表示。当开始设计这个系统时,我立即想到了一些像Erlang的基于Actor的解决方案。
由于这个系统必须在.NET上完成,所以我开始使用MailboxProcessor在F#中制作原型,但是遇到了严重的性能问题。我的最初想法是为每个用户使用一个Actor(MailboxProcessor)来序列化通信。
我已经分离出了一个小代码片段,用于复制我正在看到的问题:
open System.Threading;
open System.Diagnostics;
type Inc() =
let mutable n = 0;
let sw = new Stopwatch()
member x.Start() =
sw.Start()
member x.Increment() =
if Interlocked.Increment(&n) >= 100000 then
printf "UpdateName Time %A" sw.ElapsedMilliseconds
type Message
= UpdateName of int * string
type User = {
Id : int
Name : string
}
[<EntryPoint>]
let main argv =
let sw = Stopwatch.StartNew()
let incr = new Inc()
let mb =
Seq.initInfinite(fun id ->
MailboxProcessor<Message>.Start(fun inbox ->
let rec loop user =
async {
let! m = inbox.Receive()
match m with
| UpdateName(id, newName) ->
let user = {user with Name = newName};
incr.Increment()
do! loop user
}
loop {Id = id; Name = sprintf "User%i" id}
)
)
|> Seq.take 100000
|> Array.ofSeq
printf "Create Time %i\n" sw.ElapsedMilliseconds
incr.Start()
for i in 0 .. 99999 do
mb.[i % mb.Length].Post(UpdateName(i, sprintf "User%i-UpdateName" i));
System.Console.ReadLine() |> ignore
0
在我的四核i7上创建10万个演员大约需要800毫秒。然后将UpdateName
消息提交给每个演员并等待他们完成需要约1.8秒。
现在,我意识到所有排队的ThreadPool,设置/重置AutoResetEvents等内部MailboxProcessor都有开销。但这真的是预期的性能吗?从阅读MSDN和各种关于MailboxProcessor的博客中,我得出的想法是它类似于erlang演员,但从我所看到的可怕性能来看,这似乎在现实中不成立?
我还尝试了修改版本的代码,它使用8个MailboxProcessors,每个MailboxProcessor都持有一个Map<int,User>
映射,用于按ID查找用户,这带来了一些改进,将UpdateName操作的总时间降低到1.2秒。但它仍然感觉非常慢,修改后的代码在这里:
open System.Threading;
open System.Diagnostics;
type Inc() =
let mutable n = 0;
let sw = new Stopwatch()
member x.Start() =
sw.Start()
member x.Increment() =
if Interlocked.Increment(&n) >= 100000 then
printf "UpdateName Time %A" sw.ElapsedMilliseconds
type Message
= CreateUser of int * string
| UpdateName of int * string
type User = {
Id : int
Name : string
}
[<EntryPoint>]
let main argv =
let sw = Stopwatch.StartNew()
let incr = new Inc()
let mb =
Seq.initInfinite(fun id ->
MailboxProcessor<Message>.Start(fun inbox ->
let rec loop users =
async {
let! m = inbox.Receive()
match m with
| CreateUser(id, name) ->
do! loop (Map.add id {Id=id; Name=name} users)
| UpdateName(id, newName) ->
match Map.tryFind id users with
| None ->
do! loop users
| Some(user) ->
incr.Increment()
do! loop (Map.add id {user with Name = newName} users)
}
loop Map.empty
)
)
|> Seq.take 8
|> Array.ofSeq
printf "Create Time %i\n" sw.ElapsedMilliseconds
for i in 0 .. 99999 do
mb.[i % mb.Length].Post(CreateUser(i, sprintf "User%i-UpdateName" i));
incr.Start()
for i in 0 .. 99999 do
mb.[i % mb.Length].Post(UpdateName(i, sprintf "User%i-UpdateName" i));
System.Console.ReadLine() |> ignore
0
那么我的问题是,我做错了什么吗?我是否误解了MailboxProcessor的使用方式?或者这种性能是预期的。
更新:
所以我联系了一些在irc.freenode.net上的##fsharp的人,他们告诉我使用sprintf非常慢,而事实证明这是我的性能问题的很大一部分原因。但是,删除上面的sprintf操作并只为每个用户使用相同的名称,我仍然需要大约400毫秒来执行操作,这感觉非常慢。
Seq.initInfinite
然后构建数组所需的时间会比使用Array.init
更长,这将成为您创建时间成本的一部分。 - Leaf Garland