F# Guide — MailboxProcessor: Agent-Based Concurrency
In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
MailboxProcessor (also called agents) in F# implements the Actor Model where each agent has isolated state, communicates through message passing, and processes one message at a time for safe concurrency.
What You'll Learn
- Creating a MailboxProcessor
- Posting and receiving messages
- Discriminated unions as message types
- Shared state management
- Error handling in agents
Why It Matters
Agents eliminate shared-memory concurrency bugs by encapsulating state and processing messages sequentially. Durga Antivirus Pro uses agents for coordinating concurrent scan workers.
Real-World Use
Agents manage shared state in concurrent systems: chat servers, task queues, event processing, and state machines.
flowchart LR
A["MailboxProcessor"] --> B["Creation"]
B --> C["Messages"]
C --> D["State Management"]
D --> E["Patterns"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic MailboxProcessor
// Agent that echoes messages
let echoAgent = MailboxProcessor.Start(fun inbox ->
let rec loop () = async {
let! msg = inbox.Receive()
printfn "Received: %s" msg
return! loop ()
}
loop ())
// Send a message
echoAgent.Post "Hello agent!"
Message Types with DU
type CounterMessage =
| Increment of int
| Decrement of int
| GetCount of AsyncReplyChannel<int>
| Reset
let counterAgent = MailboxProcessor.Start(fun inbox ->
let rec loop count = async {
let! msg = inbox.Receive()
match msg with
| Increment n -> return! loop (count + n)
| Decrement n -> return! loop (count - n)
| GetCount reply ->
reply.Reply(count)
return! loop count
| Reset -> return! loop 0
}
loop 0)
// Usage
counterAgent.Post(Increment 5)
counterAgent.Post(Increment 3)
counterAgent.Post(Decrement 2)
let count = counterAgent.PostAndReply(fun reply -> GetCount reply)
// count = 6
Stateful Agent
type Task = { Id: int; Description: string; Completed: bool }
type TaskMessage =
| Add of Task
| Complete of int
| List of AsyncReplyChannel<Task list>
| Clear
let taskAgent = MailboxProcessor.Start(fun inbox ->
let rec loop tasks = async {
let! msg = inbox.Receive()
match msg with
| Add task -> return! loop (task :: tasks)
| Complete id ->
let updated = tasks |> List.map (fun t ->
if t.Id = id then { t with Completed = true } else t)
return! loop updated
| List reply ->
reply.Reply(tasks)
return! loop tasks
| Clear -> return! loop []
}
loop [])
taskAgent.Post(Add { Id = 1; Description = "Learn F#"; Completed = false })
taskAgent.Post(Add { Id = 2; Description = "Build app"; Completed = false })
taskAgent.Post(Complete 1)
let tasks = taskAgent.PostAndReply(List)
Error Handling
type SafeMessage =
| Process of string
| Error of exn
| GetStatus of AsyncReplyChannel<string>
let safeAgent = MailboxProcessor.Start(fun inbox ->
let rec loop status = async {
try
let! msg = inbox.Receive()
match msg with
| Process data ->
// Potentially failing operation
let result = riskyOperation data
return! loop (sprintf "OK: %s" result)
| Error ex ->
return! loop (sprintf "Error: %s" ex.Message)
| GetStatus reply ->
reply.Reply(status)
return! loop status
with ex ->
return! loop (sprintf "Exception: %s" ex.Message)
}
loop "Ready")
Scanning for Messages
type Message =
| Priority of string
| Normal of string
let priorityAgent = MailboxProcessor.Start(fun inbox ->
let rec loop () = async {
let! msg = inbox.Scan(fun msg ->
match msg with
| Priority _ -> Some (async {
match msg with
| Priority data -> printfn "Priority: %s" data
| _ -> ()
return! loop ()
})
| Normal _ -> None // Don't process yet
)
()
}
loop ())
// Alternative: check all messages
let! msg = inbox.Receive() // Normal order
let! msg = inbox.TryReceive(timeout=1000) // With timeout
Agent with Cancellation
let cancellableAgent ct = MailboxProcessor.Start((fun inbox ->
let rec loop () = async {
if ct.IsCancellationRequested then
printfn "Agent cancelled"
return ()
else
let! msg = inbox.Receive()
printfn "Processing: %s" msg
return! loop ()
}
loop ()), ct)
Common Mistakes
1. Blocking inside agent
Agent processes messages on one thread. Blocking operations stall the entire agent.
2. Forgetting reply channels
Use AsyncReplyChannel<'T> in messages to get responses from agents.
3. Unbounded Message Queues
Agents buffer messages in memory. Use bounded agents for backpressure.
4. Shared mutable state
Don't pass mutable state between agents. Copy data in messages.
5. Exception Handling
Unhandled exceptions crash the agent. Always catch exceptions in the message loop.
Practice Questions
1. What is a MailboxProcessor? An F# agent that processes messages sequentially in its own isolated state, implementing the actor model.
2. How do you get a reply from an agent?
Include an AsyncReplyChannel<'T> in your message type and use PostAndReply.
3. What is the difference between Receive and Scan? Receive gets the next message. Scan searches for a specific message type, potentially skipping others.
Challenge: Build a key-value store agent that supports get, set, and delete operations.
FAQ
{{< faq question="Are MailboxProcessors like Erlang actors?" >}} Yes, the model is similar. Each agent has isolated state, communicates via messages, and processes one message at a time. {{< /faq >}}
{{< faq question="How many agents can I create?" >} Thousands. Each agent uses minimal overhead. They are lightweight like Erlang processes. {{< /faq >}}
{{< faq question="What happens if an agent crashes?" >} An unhandled exception terminates the agent. Wrap the message loop in try-with to handle errors gracefully. {{< /faq >}}
{{< faq question="Can agents share state?" >} No. Each agent has its own isolated state. If they need to coordinate, they send messages to each other. {{< /faq >}}
{{< faq question="How do I stop an agent?" >} Send a Stop message or use IDisposable/CancellationToken. Agents can also simply let the loop function return. {{< /faq >}}
Mini Project
Build a logging agent with multiple log levels:
type LogLevel = Debug | Info | Warning | Error
type LogMessage = {
Level: LogLevel
Timestamp: System.DateTime
Message: string
}
type LoggerMessage =
| Log of LogMessage
| GetLogs of AsyncReplyChannel<LogMessage list>
| ClearLogs
let loggerAgent = MailboxProcessor.Start(fun inbox ->
let rec loop logs = async {
let! msg = inbox.Receive()
match msg with
| Log log -> return! loop (log :: logs)
| GetLogs reply ->
reply.Reply(List.rev logs)
return! loop logs
| ClearLogs -> return! loop []
}
loop [])
let log level message =
{ Level = level; Timestamp = System.DateTime.UtcNow; Message = message }
|> Log
|> loggerAgent.Post
What's Next
Now that you understand agents, explore query expressions for data source queries.
| Topic | Description | Link |
|---|---|---|
| F# Queries | Query expressions | {{< ref "21-queries" >}} |
| F# Type Providers | Data type providers | {{< ref "22-type-providers" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro