这是怎么回事?
都是关于蛇的。 每个人都记得蛇是什么:蛇在矩形区域上移动。 找到食物-长大,发现自己或田野边缘-死亡。 用户只能发送命令:左,右,上,下。
我决定在这里添加一些动作,使蛇从吃豆子逃走。 而这一切都在演员身上!
因此,今天,我将以蛇为例,讨论如何使用标准库中的MailboxProcessor
构建actor模型,要查找的要点以及可能遇到的陷阱。
此处编写的代码并不完美,可能违反某些原则,并且可能会更好地编写。 但是,如果您是初学者并且想处理邮箱-希望本文对您有所帮助。
如果您在没有我的情况下了解所有有关邮箱的信息,那么您可能会觉得无聊。
为什么是演员?
为了练习。 我读了演员的模特,看了视频,我喜欢一切,但我自己没有尝试。 现在我尝试了。
尽管事实上我为技术而选择技术,但这一概念非常成功地落在了这项任务上。
为什么选择MailboxProcessor,而不选择Akka.net?
对于我的任务, MailboxProcessor
是麻雀从轨道站来的, MailboxProcessor
更为简单,它是标准库的一部分,因此您无需连接任何程序包。
关于邮箱处理器和相关样板
重点很简单。 里面的邮箱有一个消息循环和某些状态。 您的消息循环将根据收到的新消息来更新此状态。
let actor = MailboxProcessor.Start(fun inbox -> // , // . inbox -- MailboxProcessor let rec messageLoop oldState = async { // let! msg = inbox.Receive()
请注意messageLoop
递归的,最后必须再次调用它,否则仅处理一条消息,此参与者将死亡。 messageLoop
也是异步的,并且在收到新消息时执行每个后续迭代: let! msg = inbox.Receive()
let! msg = inbox.Receive()
。
因此,整个逻辑负载updateState
函数承担,这意味着要创建处理器的邮箱,我们可以使构造函数接受状态更新函数和零状态:
// applyMessage // (fun inbox -> ...) let buildActor applyMessage zeroState = MailboxProcessor.Start(fun inbox -> let rec loop state = async{ let! msg = inbox.Receive() let newState = applyMessage state msg return! loop newState } loop zeroState )
好酷! 现在我们不需要经常监视,以免忘记return! loop newState
return! loop newState
。 如您所知,actor存储一个状态,但是现在完全不清楚如何从外部获取该状态。 处理器的邮箱具有PostAndReply
方法,该方法接受AsyncReplyChannel<'Reply> -> 'Msg
函数。 最初,它使我陷入僵局-完全不清楚从何处获得此功能。 但实际上,一切都变得更简单:所有消息都必须包装在DU包装器中,因为我们现在对actor进行了2种操作:发送消息本身并询问当前状态。 看起来是这样的:
// . // Mail<_,_> , Post & Get -- . // F# , // compare & equals . // -- . // [<Struct>] . type Mail<'msg, 'state> = | Post of 'msg | Get of AsyncReplyChannel<'state>
现在,我们的构造函数如下所示:
let buildActor applyMessage zeroState = MailboxProcessor.Start(fun inbox -> let rec loop state = async{ let! msg = inbox.Receive() // , // . -- , // . // -- // . ! match msg with | Post msg -> let newState = applyMessage state msg return! loop newState | Get channel -> channel.Reply state return! loop state } loop zeroState )
现在,要使用邮箱,我们需要将所有消息包装在此Mail.Post
。 为了避免每次都编写此代码,最好将其包装在一个小应用程序中:
module Mailbox = let buildAgent applyMessage zeroState = MailboxProcessor.Start(fun inbox -> let rec loop state = async{ let! msg = inbox.Receive() match msg with | Post msg -> let newState = applyMessage state msg return! loop newState | Get channel -> channel.Reply state return! loop state } loop zeroState ) let post (agent: MailboxProcessor<_>) msg = Post msg |> agent.Post let getState (agent: MailboxProcessor<_>) = agent.PostAndReply Get let getStateAsync (agent: MailboxProcessor<_>) = agent.PostAndAsyncReply Get // Single Case Discriminated Union. // MailboxProcessor API. , // - , , // . , // . type MailAgent<'msg, 'state> = MailAgent of address:string * mailbox:MailboxProcessor<Mail<'msg, 'state>> // API with member this.Post msg = // let (MailAgent (address,this)) = this Mailbox.post this msg member this.GetState() = let (MailAgent (address,this)) = this Mailbox.getState this member this.GetStateAsync() = let (MailAgent (address,this)) = this Mailbox.getStateAsync this member this.Address = let (MailAgent (address, _)) = this address member this.Dispose() = let (MailAgent (_, this)) = this (this:>IDisposable).Dispose() interface IDisposable with member this.Dispose() = this.Dispose()
我将告诉您什么address:string
稍晚一些,但是现在我们的样板已经准备好。
其实蛇
在蛇中,有一条蛇,一个带有命令的用户,一个字段和到下一帧的常规过渡。
这一切都在一起,需要我们的演员们弄脏。
我的初始布局如下:
- 演员与计时器。 接受开始/停止/暂停消息。 每n毫秒,将
Flush
消息发送给Flush
参与者。 将System.Timers.Timer
存储为状态 - 演员团队。 接收来自用户的消息,包括上
Move Up/Down/Left/Right
, AddPerk Speed/Attack
(是的,我的蛇可以快速爬网和攻击小人)和计时器Flush
。 它存储命令列表作为状态,并通过刷新清除此列表。 - 演员是蛇。 它存储蛇的状态-特权,长度,方向,弯曲和坐标。
当发现食物时,它会接收来自命令执行者的消息列表, Tick
消息(向前移动蛇1个单元格)和来自字段执行者的GrowUp
消息。 - 领域的演员。 它存储细胞图,在消息中获取蛇的状态,并在现有图片上绘制坐标。 如果游戏结束,它还会将
GrowUp
发送GrowUp
蛇演员,并将Stop
命令发送给计时器。
如您所见,即使只有这么少的实体,消息映射也已经很重要了。 在这个阶段已经出现了困难:事实是默认情况下F#不允许循环依赖。 在当前代码行中,您只能使用上面编写的代码,并且对项目中的文件也是如此。 这不是错误,而是功能,我非常喜欢它,因为它有助于保持代码整洁,但是当设计需要循环链接时该怎么办? 当然,您可以使用rec namespace
-然后在一个文件中,您可以引用此文件中使用的所有内容。
预计代码会弄乱,但随后似乎是唯一的选择。 而且有效。
外界的问题
只要演员的整个系统都与外界隔离,一切都可以正常工作,而我只在控制台中进行脱线和显示线条。 当需要以updateUI
函数的形式实现依赖关系时(应该为每个刻度重新绘制),我无法在当前实现中解决此问题。 既不丑也不美-没办法。 然后我想起了akku-毕竟,您可以在此过程中生成演员,并且在编译阶段将所有演员介绍给我。
解决方案很明显-使用akku! 不,当然,Akka仍然过高,但是我决定从那里舔一些观点-即创建一个actor系统,您可以在其中动态添加新actor并在该地址查询现有actor。
由于现在在运行时中添加和删除角色,但通过地址而不是直接链接来获取角色,因此我们需要提供一种方案,其中地址看起来不存在而角色不在那里。 按照同一acca的示例,我添加了一个用于存放死信的框,并通过我最喜欢的DU设计了它:
// Agent<_,_> -- , , // , . // , -- Box (mailagent), // , , , , // Deadbox. MailAgent, . // . // -- . type Agent<'message,'state> = | Box of MailAgent<'message,'state> | DeadBox of string * MailAgent<string * obj, Map<string,obj list>> with member this.Post msg = match this with | Box box -> box.Post msg | DeadBox (address, deadbox) -> (address, box msg) |> deadbox.Post interface IDisposable with member this.Dispose() = match this with | Box agent -> agent.Dispose() | DeadBox (_,agent) -> agent.Dispose()
系统本身看起来像这样:
// . -- . type MailboxNetwork() as this = // . ! [<DefaultValue>] val mutable agentRegister: ConcurrentDictionary<string, obj> // do this.agentRegister <- ConcurrentDictionary<string, obj>() // , // Map -- let deadLettersFn deadLetters (address:string, msg:obj) = printfn "Deadletter: %s-%A" address msg match Map.tryFind address deadLetters with // | None -> Map.add address [msg] deadLetters // -- | Some letters -> // -- Map.remove address deadLetters |> Map.add address (msg::letters) let deadLettersAgent() = ("deadLetters", Map.empty |> Mailbox.buildAgent deadLettersFn) |> MailAgent member this.DeadLetters = deadLettersAgent() // - , member this.Box<'message,'state>(address) = match this.agentRegister.TryGetValue address with | (true, agent) when (agent :? MailAgent<'message,'state>) -> // , , let agent = agent :?> MailAgent<'message, 'state> Box agent | _ -> DeadBox (address, this.DeadLetters) // -- member this.KillBox address = this.agentRegister.TryRemove(address) |> ignore member this.RespawnBox (agent: MailAgent<'a,'b>) = this.KillBox agent.Address this.agentRegister.TryAdd (agent.Address, agent) |> ignore interface IDisposable with member this.Dispose() = for agent in this.agentRegister.Values do match agent with | :? IDisposable as agent -> agent.Dispose() | _ -> ()
这是我上面写过的相同address:string
派上用场的地方。 再次奏效,现在外部依存关系很容易到达您需要的位置。 参与者的构造函数现在接受了参与者系统作为参数,并从那里获得了必要的地址:
// - ( ) - let gameAgent (mailboxNetwork: MailboxNetwork) = mailboxNetwork.Box<Command list, GameState>(gameAddress) // message loop let commandAgentFn (mailboxNetwork: MailboxNetwork) commands msg = let gameAgent = gameAgent mailboxNetwork match msg with | Cmd cmd -> cmd::commands | Flush -> commands |> gameAgent.Post []
慢慢地
出于明显的原因,在调试过程中,我将游戏设置为低速运行:滴答之间的延迟超过500毫秒。 如果将延迟减少到200,则消息开始延迟到达,并且用户团队延迟工作,这破坏了整个游戏。 美中不足的是计时器多次丢失时,计时器会收到停止命令。 对于用户而言,这丝毫没有出现,但是仍然存在某种错误。
令人不愉快的事实是,参与者当然很方便,但是直接方法调用要快得多。 因此,尽管从组织代码的角度来看,将蛇本身存储在单独的角色中很方便,但我还是不得不以速度为名放弃这个想法,因为对于游戏的1个时钟而言,消息传递太激烈了:
- 用户直接将任意数量的命令发送到命令执行者。
- 计时器将滴答声发送给团队演员,在早期实现中还发送给蛇演员,以便他将蛇移到下一个单元
- 当相应的消息来自计时器时,命令执行者发送蛇的命令列表。
- 根据2个上层消息更新了状态的蛇演员将状态发送给现场演员。
- 现场演员重画了一切。 如果蛇发现了食物,那么它将向蛇演员发送一条
GrowUp
消息,此后他将新状态发送回田野演员。
考虑到MailboxProcessor
肠道中的同步,对于所有这一切来说,只有1个时钟周期是不够的。 而且,在当前的实现中,计时器每隔n毫秒发送一次下一条消息,无论是否发生任何事情,因此,如果我们没有进行1次测量,则消息开始累积,情况变得更糟。 最好“伸展”这一特定的措施,处理所有积累的东西,然后继续前进。
最终版本
显然,应该简化消息方案,同时非常希望使代码尽可能简单且易于访问-相对而言,我不想将所有内容都推给一位上帝演员,因此演员中的意义不大。
因此,通过查看演员名单,我意识到最好先牺牲一个蛇演员。 需要一个计时器,还需要一个缓冲区来实时积累用户命令,但是每拍一次只能将其倒入一次,并且不需要客观地将蛇保留在单独的角色中,这样做只是为了方便。 此外,通过将其与现场演员保持在一起,可以立即处理GrowUp
脚本。 蛇的Tick
消息也没有多大意义,因为当我们从团队演员那里收到消息时,这已经意味着发生了新的节拍。 此外,如果有延迟,则可以延长节拍,我们进行了以下更改:
- 我们删除了“
Tick
和GrowUp
消息。 - 我们将蛇演员放到现场演员中-他现在将存储这些州的“ tapla”。
- 我们从计时器
System.Timers.Timer
删除System.Timers.Timer
。 而是,工作方案如下:当他收到“ Start
命令时,他将“ Flush
发送Flush
团队演员。 他向现场+蛇演员发送命令列表,最后一个演员处理所有这一切,并将Next
一条消息发送给计时器,从而向他请求新的滴答声。 接收到Next
的计时器将等待Thread.Sleep(delay)
并重新开始整个循环。 一切都很简单。
总结一下。
- 在先前的实现中,最小允许延迟为500 ms。 在当前延迟中,您可以将其完全删除-现场演员准备就绪时将需要新的节拍。 不再可能从以前的措施中收集原始消息。
- 消息映射大大简化了-我们拥有最简单的循环,而不是复杂的图。
- 这种简化解决了计时器丢失时多次
Stop
的错误。 - 消息列表已减少。 更少的代码-更少的邪恶!
看起来像这样:
let [<Literal>] commandAddress = "command" let [<Literal>] timerAddress = "timer" let [<Literal>] gameAddress = "game" // - let commandAgent (mailboxNetwork: MailboxNetwork) = mailboxNetwork.Box<CommandMessage, Command list>(commandAddress) let timerAgent (mailboxNetwork: MailboxNetwork) = mailboxNetwork.Box<TimerCommand, TimerState>(timerAddress) let gameAgent (mailboxNetwork: MailboxNetwork) = mailboxNetwork.Box<Command list, GameState>(gameAddress) // message loop let gameAgentFn (mailboxNetwork: MailboxNetwork) updateUi gameState cmd = let timerAgent = timerAgent mailboxNetwork // match gameState.gameFrame with // | Frame field -> // let gameState = Game.updateGameState gameState cmd timerAgent.Post Next // updateUi gameState // gameState // ! | End (Win _) -> timerAgent.Post PauseOrResume Game.updateGameState gameState cmd // | _ -> timerAgent.Post Stop // gameState // message loop let commandAgentFn (mailboxNetwork: MailboxNetwork) commands msg = let gameAgent = gameAgent mailboxNetwork match msg with | Cmd cmd -> cmd::commands // | Flush -> commands |> gameAgent.Post // [] // message loop let timerAgentFn (mailboxNetwork: MailboxNetwork) (state: TimerState) cmd = let commandAgent = commandAgent mailboxNetwork match cmd with | Start -> commandAgent.Post Flush; {state with active = true} | Next -> if state.active then // , Threading.Thread.Sleep(state.delay) commandAgent.Post Flush; state | Stop -> printfn "Stop received"; { state with active = false } | PauseOrResume -> if not state.active then //
参考文献