This chapter motivates actors from a functional-programming perspective: immutability and passing state explicitly make single-threaded programs safer, but they don’t solve the problem of sharing mutable state across threads. The actor model addresses this by partitioning an application into single-threaded actors that process messages sequentially. Actors communicate asynchronously, so concurrency concerns (queues, scheduling, synchronization) are pushed into the framework, letting business code remain referentially transparent while effects are isolated at the boundaries.
To make this concrete, the chapter builds a minimal actor framework with four pieces: Actor (the messaging API), AbstractActor (common runtime with a single-thread executor and message queue), ActorContext (exposes and can change an actor’s current behavior), and MessageProcessor (how a message is handled). Behavior changes (“become”) provide a disciplined way to model state transitions without mutable fields. After a simple ping–pong example, a more substantial manager/worker setup demonstrates parallel task distribution: a manager splits work, hands sub-tasks to workers, and aggregates results. Because parallel completion is out of order, results are first incorrectly sequenced, then fixed by tagging tasks and reordering with a priority queue; finally, a Receiver actor streams results in order, keeping internal structures small and responsive.
Performance discussion shows that actors shine when tasks are long-running and coordination costs are amortized; with very short tasks, framework overhead and naive aggregation (like inserting every result into a heap) become bottlenecks. Streaming ordered results and minimizing shared hotspots improves throughput markedly. The example also makes clear this framework’s limits (one thread per actor, no remoting) and underscores the practical takeaway: actors are most valuable for abstracting shared mutable state and synchronization while letting application logic stay immutable—use a production-grade library (for example, Akka) for real systems.
Figure 13.1. The Main actor produces the main task and sends it to the Manager actor, which splits it into subtasks that are processed in parallel by several Worker actors. Sub results are sent back to the Manager, which passes them to the Receiver. After collating the sub results, the Receiver sends the final result to the Main actor.
Figure 13.2. The Manager receives a result and either stores it in the Heap (if it doesn’t correspond to the expected number) or sends it to the client. In the latter case, it then looks at the Heap to see if the next expected result has already been received.
Summary
Actors are components that receive messages in an asynchronous way and process them one after the other.
Sharing mutable state can be abstracted into actors.
Abstracting mutable state sharing relieves you of synchronization and concurrency problems.
The actor model is based upon asynchronous messaging and is a nice complement to functional programming.
The actor model offers easy and safe parallelization.
Actor mutations are abstracted from the programmer by the framework.
Several actor frameworks are available to Kotlin programmers.
Akka is one of the most-used actor frameworks available to Kotlin programmers.
FAQ
What problem do actors solve when sharing mutable state in functional programs?Actors isolate state inside single-threaded components and communicate via messages. Because each actor processes one message at a time, you avoid locks and explicit synchronization while still coordinating concurrent work. State sharing is modeled as effects (message passing) rather than direct shared-memory mutation.How do actors communicate and what does “asynchronous messaging” mean?Actors send messages without waiting for a reply. The sender continues processing immediately; the recipient later processes the message from its queue and can respond by sending a new message back to the original sender (or another actor). The framework serializes message handling so you don’t manage queue concurrency yourself.What are the main parts of the minimal actor framework built in this chapter?- Actor: the public API; provides tell, self, shutdown, and access to context.
- AbstractActor: common implementation details (queueing, execution, default behavior).
- ActorContext: exposes behavior() and become(...) to read/replace the current behavior.
- MessageProcessor: a small interface with process(message, sender) for handling messages.How does Actor.tell work, and what is the “sender” parameter for?tell enqueues a message for the target actor to process. It takes a sender parameter of type Result<Actor<T>> so the receiver can reply. By default, sender is self(); use Actor.noSender() when there’s no sender. In this framework, tell is an effect rather than a pure function.What are ActorContext.behavior and become, and how do they model state?behavior returns the current MessageProcessor, which defines how messages are handled. become swaps the current behavior for a new one. By embedding immutable fields (e.g., queues, counters) inside a Behavior instance and replacing it with become, you achieve state transitions without mutable properties.Why is each actor single-threaded here and how are messages serialized?Each actor uses a single-threaded ExecutorService (daemon thread) and a synchronized tell to ensure that only one message is processed at a time. This serialization eliminates concurrent access to an actor’s internal state and avoids explicit locking.How do manager/worker actors parallelize a computation?A Manager actor splits work into subtasks and hands them to Worker actors. As each worker completes, it sends the result back and the Manager issues another subtask. This keeps workers busy until no tasks remain. The Manager can forward results to a dedicated Receiver actor for collation or ordering.How can results be kept in input order when tasks finish at different times?Tag each task with a sequence number. Collect results in a priority structure (e.g., a heap) keyed by that number and release them to the client when the next expected number is available. An alternative is a Receiver actor that streams results in order by comparing the expected index and temporarily storing out-of-order results, keeping the heap small.When do actors improve performance, and when do they not?Actors shine when tasks are long-running or uneven in duration—parallelism offsets messaging overhead. For short tasks, overhead (message passing, scheduling, coordination) can dominate; adding more workers beyond available cores usually gives little benefit. Misusing data structures (e.g., sorting by inserting all results into a heap) can also create bottlenecks.What are this framework’s main limitations compared to real actor systems?- One thread per actor (no dispatcher/pool), limiting scalability.
- Minimal context; no actor discovery, supervision, or advanced mailboxes.
- No transparency for remote actors or clustering.
- Bare-bones lifecycle and error handling.
Real frameworks (e.g., Akka) multiplex actors on thread pools, support supervision, remoting, routing, back-pressure, and richer tooling.
pro $24.99 per month
access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!