Agents with MailboxProcessor
Prerequisites: Async and task
Shared mutable state and threads are a recipe for race conditions. An
agent avoids the problem: it owns its state, and other code talks to
it only by sending messages, which it processes one at a time.
F#'s built-in agent is MailboxProcessor.
A counter agent
type CounterMessage =
| Increment of int
| Get of AsyncReplyChannel<int>
let counter =
MailboxProcessor.Start(fun inbox ->
let rec loop total =
async {
let! message = inbox.Receive()
match message with
| Increment n -> return! loop (total + n)
| Get reply ->
reply.Reply total
return! loop total
}
loop 0)
[ 1 .. 100 ]
|> List.map (fun i -> async { counter.Post(Increment i) })
|> Async.Parallel
|> Async.RunSynchronously
|> ignore
let total = counter.PostAndReply Get
printfn "Total: %d" total
Total: 5050
- Messages are a discriminated union — the agent's public "API".
- The state (
total) is a parameter of a recursive loop: nomutable, no locks. Postsends a message without waiting.PostAndReplysends one that carries a reply channel and waits for the answer (PostAndAsyncReplydoes the same without blocking).- A hundred concurrent posts, and the total is still exactly 5050, because the agent handles them one by one.
A cache agent
type CacheMessage =
| Set of key: string * value: string
| TryGet of key: string * AsyncReplyChannel<string option>
| Count of AsyncReplyChannel<int>
let cache =
MailboxProcessor.Start(fun inbox ->
let rec loop (store: Map<string, string>) =
async {
match! inbox.Receive() with
| Set (k, v) -> return! loop (store |> Map.add k v)
| TryGet (k, reply) ->
reply.Reply(store |> Map.tryFind k)
return! loop store
| Count reply ->
reply.Reply store.Count
return! loop store
}
loop Map.empty)
cache.Post(Set ("lang", "F#"))
cache.Post(Set ("runtime", ".NET"))
printfn "%A" (cache.PostAndReply(fun ch -> TryGet ("lang", ch)))
printfn "%A" (cache.PostAndReply(fun ch -> TryGet ("editor", ch)))
printfn "%d" (cache.PostAndReply Count)
Some "F#"
None
2
match! receives and matches in one step.
When to use agents
- Serializing access to a resource: a log file, a connection, a rate-limited API.
- Keeping state in a long-running service: in-memory caches, game state, crawler queues.
- Pipelines of workers passing messages to each other.
Agents live in one process. For distributed actors across machines,
look at Akka.NET (which has an F# API) or Microsoft Orleans.
Handle exceptions inside the loop: an unhandled exception stops the
agent (subscribe to its Error event to find out).