Concurrency Models: Actor Model vs. Communicating Sequential Processes (CSP)
Concept: The Limitations of Shared-Memory Concurrency
Shared-memory multithreading using manual locks is notoriously difficult to scale and debug.
🎯 System Scenario & Requirements
System Goal
Shared-memory multithreading using manual locks is notoriously difficult to scale and debug.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: Shared-memory multithreading using manual locks is notoriously difficult to scale and debug.
- To avoid race conditions and deadlocks, modern platforms use message-passing concurrency models.
- This guide compares the Actor Model (used in Erlang and Akka/Pekko) with Communicating Sequential Processes (CSP, used in Go) and provides full Java implementations for both.
Constraints & Assumptions
- In the classic multi-threaded programming model, threads communicate by sharing access to the same heap memory space.
- To prevent concurrent threads from overwriting each other's data (race conditions) or reading partial writes (inconsistent states), developers must use synchronization primitives like locks, mutexes, or semaphores.
- Mailbox Overflow: If an actor processes messages slower than they arrive, its mailbox will grow indefinitely, leading to an OutOfMemoryError .
- Mitigation: Use bounded mailboxes and configure drop-oldest or dead-letter queue policies.
- Passing Mutable Objects in Messages: Message-passing models only guarantee thread safety if messages are immutable .
- If Actor 1 sends a mutable Java List to Actor 2, both threads will hold a reference to that list, restoring shared-memory concurrent write bugs.
⚡ Execution Stages & Decision Trace
Stage 1 — Mechanics: Mailboxes, Processes, and Message Passing
To implement message passing, we choose between two primary concurrency models: 1\.
The Actor Model In the Actor Model, the fundamental unit of computation is the Actor .
Deep Dive
Scenario context
Shared-memory multithreading using manual locks is notoriously difficult to scale and debug. To avoid race conditions and deadlocks, modern platforms use message-passing concurrency models. This guide compares the Actor Model (used in Erlang and Akka/Pekko) with Communicating Sequential Processes (CSP, used in Go) and provides full Java implementations for both.
In the classic multi-threaded programming mode…
This walkthrough focuses on Concept: The Limitations of Shared-Memory Concurrency.
Before making a design choice, clarify:
- In the classic multi-threaded programming model, threads communicate by sharing access to the same heap memory space.
- To prevent concurrent threads from overwriting each other's data (race conditions) or reading partial writes (inconsistent states), developers must use synchronization primitives like locks, mutexes, or semaphores.
- However, manual shared-memory locking introduces major architectural challenges: Deadlocks: If Thread 1 holds Lock A and waits for Lock B, while Thread 2 holds Lock B and waits for Lock A, both threads will hang indefin…
Concepts in play: jvm-internals-and-garbage-collection, stack-and-queue-data-structures, circuit-breaker, message-queues
Stage 1 — Mechanics: Mailboxes, Processes, and Message Passing
- To implement message passing, we choose between two primary concurrency models: 1.
- The Actor Model In the Actor Model, the fundamental unit of computation is the Actor .
- An actor is a self-contained object that encapsulates state, behavior, and a private message queue called a Mailbox .
- Actors communicate exclusively by sending asynchronous messages to other actors' mailboxes.
Observable outcome: Actors communicate exclusively by sending asynchronous messages to other actors' mailboxes.
Stage 2 — Flow: Message Passing Sequence
- The diagram below visualizes the architectural differences between the Actor Model's address-based routing and CSP's channel-based routing: The table below summarizes the key structural differences between the two model…
flowchart TD
subgraph Actor Model: Address-Based
Actor1[Actor 1] -->|Send Message to Address| Mailbox2[Mailbox 2]
Mailbox2 -->|Sequentially Process| Actor2[Actor 2]
end
subgraph CSP Model: Channel-Based
Proc1[Process 1] -->|Write to Channel| Channel[Shared Channel]
Channel -->|Read from Channel| Proc2[Process 2]
end
Observable outcome: The diagram below visualizes the architectural differences between the Actor Model's address-based routing and CSP's channel-based routing: The table below summarizes the key structural differences between the two model…
Stage 3 — Deep Dive: Structural Isolation & Thread Context Switches
- Let us analyze the internal queue mechanics, mathematical models, and performance constraints of message-passing concurrency.
- Thread Isolation and Queue Internals In both models, message passing relies on thread-safe queues.
- In the Actor Model, mailboxes are typically implemented using non-blocking queues (like Java's ConcurrentLinkedQueue ) to allow publishers to push messages without blocking.
- In CSP, channels are implemented using blocking queues (like Java's ArrayBlockingQueue with a capacity boundary) to enforce backpressure.
Observable outcome: In CSP, channels are implemented using blocking queues (like Java's ArrayBlockingQueue with a capacity boundary) to enforce backpressure.
Stage 4 — Advanced Concepts: Supervision Trees and Channel Buffering
- A key advantage of the Actor Model is its Supervision Model (pioneered by Erlang).
- Actors are organized in a hierarchy.
- If an actor throws an uncaught exception, its parent supervisor detects the failure and applies a recovery policy: One-for-One: Restart only the failed actor.
- One-for-All: Restart all child actors in the supervisor group.
Observable outcome: One-for-All: Restart all child actors in the supervisor group.
Stage 5 — Applications: From Erlang Systems to Go Channels
- High-Frequency Chat Backends (Erlang/Akka): Chat servers represent each user connection as a dedicated actor, managing state and messaging in isolation.
- Go Web Server Pipelines (CSP): HTTP requests are handled by lightweight goroutines communicating via channels to execute logging, authentication, and database updates in parallel.
- Telecommunication Switches: Managing thousands of simultaneous calls using isolated actors to prevent call failures from affecting the system.
Observable outcome: Telecommunication Switches: Managing thousands of simultaneous calls using isolated actors to prevent call failures from affecting the system.
End-to-end outcome
After working through the stages, you should be able to explain:
- Concurrency Proverb: Share memory by communicating, rather than communicating by sharing memory.
- Actor Model: Encapsulates state and mailbox.
Alternatives considered
Decision Guide: Shared Memory vs. Actors vs. CSP
Decision Guide: Shared Memory vs. Actors vs. CSP
Let us implement both patterns in Java.
Let us implement both patterns in Java.
1. Lightweight Actor System in Java
This implementation defines an actor structure using Java's ExecutorService and ConcurrentLinkedQueue.
2. CSP Model using BlockingQueue Channels
This implementation defines a Producer-Consumer pipeline using Java's ArrayBlockingQueue to represent a CSP channel.
Failure and degraded modes
Concept: The Limitations of Shared-Memory Concurrency
- Detection: In the classic multi-threaded programming model, threads communicate by sharing access to the same heap memory space.
- User or operator impact: In the classic multi-threaded programming model, threads communicate by sharing access to the same heap memory space.
Trade-offs and Failure Modes
- Detection: Mailbox Overflow: If an actor processes messages slower than they arrive, its mailbox will grow indefinitely, leading to an OutOfMemoryError .
- User or operator impact: Mailbox Overflow: If an actor processes messages slower than they arrive, its mailbox will grow indefinitely, leading to an OutOfMemoryError .
Lessons Learned: Common Concurrency Pitfalls
- Detection: Passing Mutable Objects in Messages: Message-passing models only guarantee thread safety if messages are immutable .
- User or operator impact: Passing Mutable Objects in Messages: Message-passing models only guarantee thread safety if messages are immutable .
