25. Concurrency
Overview
Fantom tackles concurrency using a very different path from Java and C#. Java and C# use a shared memory model - all threads have access to each other's memory space. Synchronization locks are required to ensure that threads share data in a consistent manner. This concurrency model is quite powerful, but operates at a low level of abstraction. As such, even skilled programmers have a hard time writing code which is free of both race conditions and deadlocks. This model also makes it very hard to create composable systems because all system components must orchestrate their use of locking consistently.
The Fantom model of concurrency is based upon the following principles:
- No Shared Mutable State: threads never share mutable state under any circumstances;
- Immutability: the notion of immutability is embedded into the language itself. Immutable data can be efficiently and safely shared between threads (for example via a static field);
- Message Passing: the actor API is built around the idea of passing messages between asynchronous workers;
Immutability
An object is said to be immutable if we can guarantee that once constructed it never changes state. Fantom supports these types of immutable objects:
- Any object instance of a const class
- The result of
Obj.toImmutable
on a List, Map, Buf, or Func
By definition a const class is immutable - the compiler verifies that all the instance fields are themselves immutable and only set in the object's constructor.
Func
objects are determined as mutable or immutable by the compiler depending on if the function captures mutable state from its environment. See Functions for more details.
Memory backed Buf
objects can made immutable by calling the toImmutable
method. The original bytes are transfered to a new readonly Buf instance.
The toImmutable
method supported by List
and Map
is a mechanism to return a readonly, deep copy of the collection which ensures all that all values and keys are themselves immutable. The compiler will allow assignment to const List/Map fields during construction, but it implicitly makes a call to toImmutable
. For example to declare a const list of strings:
// what you write class SouthPark { const static Str[] names := ["Stan", "Cartman", "Kenny"] } // what the compiler generates class SouthPark { const static Str[] names := ["Stan", "Cartman", "Kenny"]?.toImmutable }
You can check if an object is immutable via the Obj.isImmutable
method.
Shared Objects
Actors allow objects to be shared between threads. Specific APIs which will pass an object to another thread include:
All of these APIs use the same pattern to safely pass an object between threads. If an object is immutable then we can safely pass it to another thread by reference. Otherwise we assume the object is serializable and pass a serialized copy of the object to another thread. Both of these approaches have their pros and cons to consider in your application design:
- immutable:
- ideal for simple structures if fast to create
- deeply structured objects expensive to change inside a single thread
- always extremely efficient to pass between threads
- serializable:
- deeply structured objects can be modified efficiently in a single thread
- moderately expensive to pass between threads