How To Share Your Data Between Threads
In the article Confused About Const Classes? I talk about how Fantom classes adorned with the keyword const
are constant and how the data they hold can't be changed. Well, that was all lies (sort of!) and in this article I'll tell you how const
classes can hold mutable (changeable) data.
The key word in the last sentence was "hold". User defined const
classes are still constant and the immediate values and object references they hold can not change. But there are some system const
classes that can hold mutable state, and our const
classes can hold references to these.
Why?
Why would you want a const
class to hold mutable state anyway? The answer is Concurrency.
Only instances of const
classes can be used by multiple threads.
"So what?" you say. To give a real example let's say you have a shopping web site built with BedSheet.
- When a person logs in, a
User
object is returned from aUsers
service. - That
Users
service needs to be accessed from every web request. - But every web request is handled by a (potentially) different thread.
- Ergo, the
Users
object needs to be aconst
class.
For that Users
object to keep track of new and deleted users, it needs to reference mutable state. So now, let me re-define what const
really means:
const
means the class is thread-safe.
That statement is really important, and the rest of the article is based on that very principle.
So now, on to sharing mutable state between threads and actors! If the very notion seems daunting and scary, fear not, for here are some easy examples:
- The Problem
- Use an Unsafe object
- Use an AtomicRef
- Use an AtomicMap
- Use an Actor
- Use a SynchronizedMap
- Epilogue
The Problem
First let's take a closer look at the User
and Users
classes and try to hold the user data in a basic Map
:
const class Users {// --> Compilation error here!private Str:User userMap := [:] User getUser(Str name) { return userMap[name] } Void setUser(Str name, Int iq) { user := User() { it.name = name; it.iq = iq } userMap[name] = user } } const class User { const Str name; const Int iq new make(|This| f) { f(this) } }
Not so good; we get the follwing compilation Err:
Const type 'Users' cannot contain non-const field 'users'
Hmm... so what can we replace the Map
with?
Use an Unsafe object
The easiest solution, but by far the most dangerous, is to wrap the map in an Unsafe object. Unsafe
is a const
system object that lets you wraps mutable, non-const objects. Thus effectively by-passing the entire thread-safety mechanism built-in to Fantom.
const class Users {// Danger Will Robinson, Danger!!!private const Unsafe unsafeMap := Unsafe(Str:User[:]) User getUser(Str name) { userMap := (Str:User) unsafeMap.val return userMap[name] } Void setUser(Str name, Int iq) { user := User() { it.name = name; it.iq = iq } userMap := (Str:User) unsafeMap.val userMap[name] = user } } const class User { const Str name; const Int iq new make(|This| f) { f(this) } }
Unsafe objects are, as the name suggests, unsafe! They allow unfettered access to the wrapped object, resulting in inconsistent state, data loss and possible concurrent exceptions.
The bottom line. Do not use them.
Use an AtomicRef
Alternatively, you can wrap the map in an AtomicRef. AtomicRef
is a const
class that holds a reference to an object. You may get and set the object reference as many times as you like, with the caveat that the object(s) in question are const
themselves.
In our example, we can store an immutable Map
, that holds instances of our const User
class:
using concurrent::AtomicRef const class Users {// AtomicRefs subject to data loss in race conditionsprivate const AtomicRef atomicMap := AtomicRef(Str:User[:].toImmutable) User getUser(Str name) { userMap := (Str:User) atomicMap.val return userMap[name] } Void setUser(Str name, Int iq) { user := User() { it.name = name; it.iq = iq } userMap := (Str:User) atomicMap.val// (1)rwMap := userMap.rw// (2)rwMap[name] = user// (3)roMap := rwMap.toImmutable// (4)atomicMap.val = roMap// (5)} } const class User { const Str name; const Int iq new make(|This| f) { f(this) } }
AtomicRefs
, along with AtomicInts and AtomicBools, are great for sharing bits of constant immutable data between threads. The code for getUser()
is both safe and fast as we're returning values from the same immutable map.
But problems occur during setUser()
. Because AtomicRefs
only store immutable data, in our example a read-only immutable Map
, to add a value to the map we need to:
- read the immutable map from the
AtomicRef
- make a read / writable copy of the map
- add our data
- make an immutable copy of the map
- overwrite the map value held in the
AtomicRef
There are two issues with doing this:
1. It is slow
We take two (shallow) copies of the entire map. This may be fine when the map is small, but for large maps it could be a performance hit.
The AtomicRef
method of sharing data is good for when reads far out number the writes and it is exactly how Java's CopyOnWriteArrayList works.
2. Data loss occurs during race conditions
If two threads call setUser()
at the same time, they'll both take a copy of the same map, adding their value to it. The first thread will update the AtomicRef
, and the second will then overwrite it; effectively removing the value added by the first.
Data loss in our case is scary, because we could loose users! But if the Map
is being used purely as a cache of computed values (to store, say, digests of files), then this isn't really a problem... just a minor resource waste as the digest would need to be recalculated if it didn't exist.
Use an AtomicMap
If the thought of using an AtomicRef
is appealing but looks like too much work, then consider using an AtomicMap from Alien-Fatory's Concurrent library.
AtomicMap
is a pseudo-Map (has similar methods to a standard Map
) but has all data backed by a Map
held in an AtomicRef
. It works in exactly the same way as described above.
Using one, our example would look like:
using afConcurrent::AtomicMap const class Users { private AtomicMap userMap := AtomicMap() User getUser(Str name) { return userMap[name] } Void setUser(Str name, Int iq) { user := User() { it.name = name; it.iq = iq } userMap[name] = user } } const class User { const Str name; const Int iq new make(|This| f) { f(this) } }
The Concurrent library also has a matching AtomicList that works in a similar manner.
Use an Actor
Threads are allowed to hold mutable state (obviously), it's just the data that is passed to and from them that needs to be const
. So what if we held our Map
in a different thread, and made synchronous calls to that thread to get and set our User
objects?
Woah! Like what!? Like this:
A simple implementation in code then looks like:
using concurrent::Actor using concurrent::ActorPool const class Users { private const Actor dataThread := Actor(ActorPool()) |Obj obj->User?| { return runInThread(obj) } User getUser(Str name) { return dataThread.send(name).get } Void setUser(Str name, Int iq) { user := User() { it.name = name; it.iq = iq } dataThread.send(user).get } private User? runInThread(Obj obj) { userMap := (Str:User) Actor.locals.getOrAdd("users.userMap") { [:] } if (obj is Str) { name := (Str) obj return userMap[name] } if (obj is User) { user := (User) obj userMap[user.name] = user } return null } } const class User { const Str name; const Int iq new make(|This| f) { f(this) } }
dataThread
is the Actor
that holds our data. All messages sent to it call runInThread()
which is executed inside the Data Thread. Our Map
of users is kept in Actor.locals()
. As it is a large topic in itself, I'll not say much more about Actors
and Actor.locals()
.
Because User
objects are passed to and from our Data Thread they need to be const
(or Serialisable), which isn't a problem for us. In fact, because User
objects are const
they are passed by reference meaning the above code runs very, very fast. The code executes practically unhindered.
As runInThread()
runs in it's own thread, all calls to it are executed one at a time; as is the nature of Actors
. So if we have 10 threads calling setUser()
at the same time, the 10th thread is blocked until all the other threads have finished their call to runInThread()
.
Calling runInThread()
is essentially the same as using the synchronized
key word in Java and is just as thread safe.
Use a SynchronizedMap
If the thought of using a Actors
to store data is appealing but looks like too much work, then consider using a SynchronizedMap from Alien-Fatory's Concurrent library.
SynchronizedMap
is a pseudo-Map (has similar methods to a standard Map
) but has all data backed by a Map
held in an Actor
. It works in exactly the same way as described above.
Using one, our example would look like:
using afConcurrent::SynchronizedMap using concurrent::ActorPool const class Users { private SynchronizedMap userMap := SynchronizedMap(ActorPool()) User getUser(Str name) { return userMap[name] } Void setUser(Str name, Int iq) { user := User() { it.name = name; it.iq = iq } userMap[name] = user } } const class User { const Str name; const Int iq new make(|This| f) { f(this) } }
The Concurrent library also has a matching SynchronizedList that works in a similar manner. It also has classes that provide generic synchronized
access to blocks of code, and to arbitrary mutable state.
Epilogue
In a multi-threaded application, such as a web application, only instances of const
classes may be shared between threads (and / or HTTP requests). For these const
classes to be useful, they need access to mutable state.
We looked a couple of options involving Fantom's core Unsafe
, AtomicRef
and Actor
classes. We also saw how Alien-Factory's Concurrent library provides useful utility classes for doing the same thing.
Now you know how to store useful data in const
classes you can create system Services, IoC Services and multi-threaded caches!
Have fun!