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:
    • Volatile field changes are atomic and immediately visible to other threads
    • Non-volatile field 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:
  • 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

AspectImmutable Reference Message PassingMutable Reference Message Passing
Core PrincipleMessages are copies or immutable references that cannot be changed after sendingMessages 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 SupportMany 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 ManagementCan be complex to coordinate resource cleanupOften clearer due to explicit ownership
Example LanguagesErlang/Elixir, Clojure, Haskell, Scala (Akka)Rust, Go (with discipline)
Code ComplexityOften requires more message-handling codeCan be more concise for large data structures

Key Differences

  1. 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
  2. Mental Model
    • Immutable: “Share by communicating” - data is copied or shared immutably
    • Mutable: “Communicate by sharing” with ownership transfer - data references move between processes
  3. 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
  4. 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:
  • 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