Stately, a Kotlin Multiplatform Library
This started as a monster single post, now split in 2. Part 1, Saner Concurrency, is about what Kotlin is doing with concurrency.
During my talk at KotlinConf, I promised a part 2 of Stranger Threads to better explain threading and state in Kotlin/Native. I built a library instead.
What is Stately
Stately is a collection of structures and utilities designed to facilitate Kotlin/Native and multiplatform concurrency. As of today, it is a set of expect/actual definitions that most apps will wind up needing, and a set of frozen, sharable collection classes that allow you to maintain mutable collections across threads.
Why does it exist?
Kotlin/Native, and hopefully soon, all of Kotlin, will be implementing Saner Concurrency. Native, today, has runtime rules, and notably a lack of “standard” concurrency primitives, that help ensure runtime concurrency sanity. The 2 basic rules are:
- All mutable state is in one thread. It can be transferred, but is “owned” by one thread at a time
- All shared state is immutable (frozen)
These are good rules, because they will make concurrent code safer.
However, there are times where being able to access shared, mutable state is pretty useful. That may change as we adapt architectural thinking, but at least for today, there have been a few tricky situations to arise without that available. Global service objects and caches, for example.
Kotlin/Native has a set of Atomic classes that allow you to mutate state inside of an immutable (frozen) object. The Stately collections are constructed with atomics.
For the most part, I’d expect Stately to be used sparingly, if at all, but it’s one of those things that you’ll really miss if you need it and don’t have it.
What you shouldn’t use it for
Kotlin/Native has rules that help encourage safer concurrency. Changing frozen state is, on some level, enabling manually managed concurrency. There are practical reasons to have these collections available, but if you’re using them a lot, it might be better to try for some architectural changes.
If you’re new to Native and running into mutability exceptions, you might be tempted to make everything atomic. This is equivalent to marking everything synchronized in Java.
It’s important to understand how Native’s threading and state work, and why you need a concurrent collection. But, you know, no judgements.
The collections mostly act like their mutable counterparts. You designate generic types, and get/set/remove data entries. One key thing to note.
Anything you put into the collection will get frozen.
That is very important to understand. The collection itself is “mutable” in the sense that you can add and remove values, but the collection and values it holds are all frozen.
For data objects, this is generally OK, but for callbacks, this may be somewhat mind bending to understand. That’s not a Stately problem so much as a Kotlin/Native problem.
I’ve talked to several people who struggle with this reality. All I can say is it seems weird at first, but is not as big of a deal as you think. You just really need to be aware of what that means.
For example, in the Droidcon app, we’re using LiveData to build a reactive architecture. To avoid freezing everything in the Presenter/UI layer, we simply keep all of our callbacks thread local to the UI thread, which means they don’t need to be frozen. The lower level callbacks all exist in Sqldelight, and are frozen, but they exist just to push data back to the main thread and don’t capture UI-layer state.
Summary, it’s different, but not that bad. The details would turn this blog post back into a monster, so I’ll just push out my promised “Stranger Threads” deadline out by a couple weeks.
A lot of how we think about architecture will change as coroutines mature on Native, and as the community has some time to think about the implications. I suspect there will be less use for shared collections as that happens, but for today, they’re pretty useful.
Similar to Java’s CopyOnWriteArrayList. In fact, the JVM implementation is Java’s CopyOnWriteArrayList. Useful in cases with infrequent changes and critical read stability. Registering callback listeners, for example. If you’re changing the list often, or it’s large, each edit requires a full copy. Something to keep in mind.
This is a mutable list that will have reasonable performance. All edits lock, just fyi, but individual edits themselves are pretty small, so locks are quick. You can hold onto a node reference that will allow you to swap or remove values without traversing the list, which is important in some cases.
There are 2 basic flavors. One has unstable but performant iterations, the other I’d call CopyOnIterateList. It’s similar to CopyOnWriteList, except the copy happens when you call iterate. This may prove more generally useful than COWAL, as it should handle frequent changes better, but if you’re editing often and iterating often, remember you’re dealing with an aggressively locking data structure.
The other flavor will let you iterate without copying or locks, but edits to the list will be reflected while you’re iterating. The edits are atomic, so you won’t get errors, but you’ll also wind up with potentially fuzzy iterations. If that isn’t an issue, this should be a better choice.
I am aware that there are lockless implementations of linked list, but I didn’t attempt one. Going for simple.
Implements a pretty basic version of our favorite structure HashMap. Performance should have similar characteristics to what we’re used to from Java🌶, although obviously locking will produce different absolute numbers. In short, same big O.
I probably don’t need to go into where a hash map would be useful, but in the service object context, it’s probably caches, which leads to…
Shared version of a least recently used cache. If you’re not familiar, there’s a cap on the number of values, and the “oldest” get bounced first. “Oldest” being defined as the least recently accessed.
This is pretty new. As Kotlin 1.3 matures, we should have a more stable deployment, and may add some other features. I’ll try to add some issues for “help-wanted” in case anybody wants to contribute.
Also, as of today (Friday 10/26) the JS code has an implementation but tests need to be wired in. That means don’t use the JS until that happens.
The only supported Native implementation is for mac and iOS. Other Native targets should work, except we’ll need to find a lock implementation. Pthread_mutex is fine, except you need to destroy it explicitly, and K/N has no destructor. That means a ‘close’ method on the collection, which I’d rather avoid. Right now there’s a spin lock, but not sure if that’s a great idea.
The collection implementations could be described as minimal. My main goal was to start replacing some of the custom C++ code we’d put into earlier K/N implementations, which generally existed because of a need for shared state. If there’s a real need for something else, open an issue to discuss.
🌶 Similar to Java 7 and below. That is, if your bucket size doesn’t increase, as your entry size increases, or you have a bad hash, you start to approach N, because the bucket list means a lot of scanning. In Java 8, after the list gets to size 8(ish), it’ll store those values as a tree, so worst case gets capped. Our hash map resizes like Java’s, so you should only find yourself in “worst case” if your hash is bad, or if you have horrible settings on your map. You probably don’t need to know this, but Java 8’s optimization is kind of cool, and I didn’t bother trying to implement it 🙂