Implications of Multicore Hardware
- Memory access no longer uniform
- Each core has its own cache with different view of memory
- Need for programming language memory models (see Functional Programming for referential transparency)
- Java memory model as an example:
Volatilefield changes are atomic and immediately visible to other threads- Non-
volatilefield changes are done while holding a lock, only become visible to other threads when they pick up the lock - New threads see the system as if they had just picked up the lock
- If a thread terminates, all of its changes immediately become
- 32bit fields are all atomic (can’t complete a halfway read or write)
- Java is unique for having such a clearly specified memory model, C/C++ and Rust don’t
Problems with Thread-Based Concurrency
- Hard to define memory models that allow both performance and reasoning
- Difficult to ensure correct locking
- Failures are silent and often load-dependent
- Locking doesn’t compose well (individual components may be correct but not their combination)
- See also Concurrency and Concurrency vs Parallelism
Alternative 1: Atomic Transactions
- Structure programs as atomic blocks that succeed or fail together
- Follows ACID properties (Atomicity, Consistency, Isolation, Durability)
- Implementation uses optimistic transactions with rollback
- Requires referential transparency (see Functional Programming) and control over I/O
- Works well in Haskell (see Monad Transformers) but difficult in mainstream languages
Alternative 2: Message Passing
Immutable Message Passing
- Structure system as communicating processes/actors
- No shared mutable state (see Functional Programming for immutability)
- Communication through immutable messages
- Two main architectures:
- Dynamically typed with direct messaging (Erlang, Akka)
- Statically typed with explicit channels (see Type Systems and Rust Programming Language)
- Avoids many problems of shared state concurrency
- Popular in Erlang and Elixir
Mutable Message Passing
use std::sync::mspc::channel;
use std::thread;
fn main() {
let (tx, rx) = channel();
thread::spawn(move|| {
let _ = tx.send(42);
});
match rx.recv() {
Ok(value) => {
println!("Got {} ", value);
}
Err(error) => {
// Handle Error
}
}
}Comparison
| Aspect | Immutable Reference Message Passing | Mutable Reference Message Passing |
|---|---|---|
| Core Principle | Messages are copies or immutable references that cannot be changed after sending | Messages include references to mutable data with clear ownership semantics |
| Safety | ✓✓✓ High - No shared mutable state eliminates most concurrency hazards | ✓✓ Medium - Safety depends on ownership rules and discipline |
| Performance | ✓✓ Medium - May require copying data between processes | ✓✓✓ High - Can avoid copies by transferring references |
| Memory Usage | ✓ Lower - May duplicate data across processes | ✓✓✓ Higher - Can share single instances across processes |
| Reasoning | ✓✓✓ Simple - Once received, message cannot change unexpectedly | ✓✓ More complex - Must track ownership to understand data flow |
| Distribution | ✓✓✓ Natural - Fits distributed systems well | ✓ Limited - Challenges with shared references across nodes |
| Language Support | Many languages (Erlang, Elixir, Clojure, functional languages) | Fewer languages with strong ownership semantics (Rust, some features in Go) |
| Error Isolation | ✓✓✓ Strong - Process failures don’t corrupt shared data | ✓✓ Moderate - Depends on ownership transfer implementation |
| Resource Management | Can be complex to coordinate resource cleanup | Often clearer due to explicit ownership |
| Example Languages | Erlang/Elixir, Clojure, Haskell, Scala (Akka) | Rust, Go (with discipline) |
| Code Complexity | Often requires more message-handling code | Can be more concise for large data structures |
Key Differences
- Safety vs. Performance Trade-off
- Immutable: Prioritizes safety and correctness at some performance cost
- Mutable: Prioritizes performance and resource efficiency with more complex safety guarantees
- Mental Model
- Immutable: “Share by communicating” - data is copied or shared immutably
- Mutable: “Communicate by sharing” with ownership transfer - data references move between processes
- Ideal Use Cases
- Immutable: Distributed systems, fault-tolerant applications, systems where correctness trumps performance
- Mutable: Performance-critical applications, systems processing large data structures, resource-constrained environments
- Implementation Requirements
- Immutable: Relies on language support for immutable data structures
- Mutable: Relies on strong ownership semantics or disciplined programming
Race Conditions
- In message passing systems: order of message arrival
- In shared memory systems: data races when modifying shared values
- Solutions:
- Immutable data (Erlang approach, see Functional Programming)
- Ownership tracking (Rust approach, see Resource Ownership and Memory Management)
- Garbage Collection systems often us a shared “exchange heap” which means that it has a separate memory area for these shared memory, expensive to garbage collect because it requires synchronisation
Erlang’s Approach to Robustness
- “Let it crash” philosophy
- Processes are cheap and plentiful
- Supervisor hierarchy for error handling (see Error Handling)
- Focus on system reliability rather than individual component reliability