How To Share Your Data Between Threads

Share Your 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 a Users 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 a const 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

First let's take a closer look at the User and Users classes and try to hold the user data in a basic Map:

select all
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.

select all
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:

select all
using concurrent::AtomicRef

const class Users {

    // AtomicRefs subject to data loss in race conditions
    private 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:

  1. read the immutable map from the AtomicRef
  2. make a read / writable copy of the map
  3. add our data
  4. make an immutable copy of the map
  5. 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:

select all
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

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:

Actor Objects

A simple implementation in code then looks like:

select all
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().

Multi-Threaded Calls

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:

select all
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!


Discuss