Building a Worker Pool in F# with MailboxProcessor

Search for a command to run...

No comments yet. Be the first to comment.
Modern software systems often separate control logic from high-performance execution logic. This design is common in networking, distributed systems, operating systems, storage engines, and embedded s

Introduction Engineering software often combines multiple programming languages to leverage their individual strengths. A common approach is to implement computational algorithms in native C or C++ wh

Deadlocks are one of the most common synchronization problems encountered in operating systems and concurrent programming. Although the concept is frequently introduced in textbooks, observing it insi

Memory and resource allocation are fundamental operations inside the Linux kernel. Whether assigning device IDs, managing CPU masks, allocating interrupt vectors, or tracking hardware resources, the k

The Linux scheduler is one of the most important components of the operating system. Every running program, background service, and kernel thread eventually interacts with the scheduler. In this artic

While studying concurrent network programming, I explored how to implement a worker pool in F# using MailboxProcessor, F#'s built-in actor abstraction.
The example launches multiple workers that process port numbers concurrently. A CountdownEvent is used to wait until all work items have been completed before shutting down the workers gracefully.
This approach is conceptually similar to Go's goroutines and channels but follows a more actor-oriented design.
The implementation consists of three main pieces:
| Component | Responsibility |
|---|---|
| MailboxProcessor | Worker actor |
| Message Type | Communication protocol |
| CountdownEvent | Completion tracking |
Workers communicate through strongly typed messages.
type Message =
| Port of int
| Stop
This provides a clear protocol:
Port → process a port number
Stop → terminate the worker
Unlike string-based messaging, the compiler verifies message correctness.
Each worker runs as an independent actor.
let worker (id: int) =
MailboxProcessor.Start(fun inbox ->
let rec loop () = async {
let! msg = inbox.Receive()
match msg with
| Port p ->
printfn "Worker %d processed port %d" id p
return! loop ()
| Stop ->
()
}
loop ()
)
Important characteristics:
Sequential message processing
No explicit locking
Independent execution context
Message-driven behavior
To emulate Go's WaitGroup, the example uses CountdownEvent.
let completed = CountdownEvent(1024)
Each processed port decrements the counter.
completed.Signal() |> ignore
The main thread blocks until all tasks are finished.
completed.Wait()
A pool of 100 workers is created.
let workers =
[|
for i in 1 .. 100 ->
worker i
|]
Each worker owns its mailbox and processes incoming messages independently.
Port numbers are distributed across workers.
for port in 1 .. 1024 do
let index = port % 100
workers.[index].Post(Port port)
This creates a simple round-robin scheduling strategy.
After all work has completed, a termination message is sent.
for w in workers do
w.Post(Stop)
This avoids abruptly terminating active workers.
The design maps naturally to concepts familiar to Go developers.
| Go | F# |
|---|---|
| Goroutine | MailboxProcessor |
| Channel | Mailbox |
| WaitGroup | CountdownEvent |
| Send Message | Post |
| Close Channel | Stop Message |
Although the implementation style differs, both approaches rely on message passing rather than shared-state synchronization.
MailboxProcessor offers several advantages:
Actor-style concurrency
Strongly typed messages
No manual lock management
Clear separation of worker responsibilities
Scales well for network and backend services
This makes it a useful building block for:
Port scanners
TCP servers
Telemetry systems
Chat applications
Distributed services
IoT backends
F#'s MailboxProcessor provides a concise and powerful way to implement concurrent worker pools. By combining actors with typed messages and synchronization primitives such as CountdownEvent, I can build systems that are both scalable and easier to reason about than traditional shared-memory approaches.