Redux on Multithreaded Platforms
Writing code with Redux for web really doesn’t require consideration of threading and concurrent modification of variables. Given Javascript is single threaded its just not an issue. Working in JVM or Native suddenly presents a new set of challenges.
Concurrent modification and access of shared memory can result in modifications happening out of order, or reads returning data that should have been updated. A simple counter example can demonstrate this by incrementing the counter from different threads and checking the result. Jetbrains has an excellent example using coroutines.
To demonstrate the same counter example with Redux consider the following unit test:
object MultiThreadedSpec : Spek({ describe("createStore") { it("multithreaded increments massively") { suspend fun massiveRun(action: suspend () -> Unit) { val n = 100 // number of coroutines to launch val k = 1000 // times an action is repeated by each coroutine val time = measureTimeMillis { coroutineScope { // scope for coroutines repeat(n) { launch { repeat(k) { action() } } } } } println("Completed ${n * k} actions in $time ms") } val store = createStore(counterReducer, TestCounterState()) runBlocking { withContext(Dispatchers.Default) { massiveRun { store.dispatch(Increment()) } } assertEquals(100000, store.state.counter) } } } }) class Increment data class TestCounterState(val counter: Int = 0) val counterReducer = { state: TestCounterState, action: Any -> when (action) { is Increment -> state.copy(counter = state.counter + 1) else -> state } }
Running this test fails horribly:
isDispatching
in the dispatch function. That check was disabled for this test.Clearly a big problem! For apps using some redux libraries this could result in an invalid state, or crashes that happen randomly and are difficult to reproduce.
How to handle global store with Concurrency?
So there are 3 options to handling this situation:
- Live in the Wild West and do nothing
- Do all interactions with the store on same thread
- Synchronize access to the store
Originally, ReduxKotlin did not provide a solution(#1), and it was up to developers to use how they wish. After talking with devs using Redux it was clear that not providing a solution in the library was a problem.
Starting with ReduxKotlin version 0.3.0 – 0.4.0 #2 was forced upon developers. Same thread enforcement was added to the Redux store. This meant any interaction with the store’s functions on threads other than the thread from which it was created would throw an IllegalStateException. The idea here was this hard rule would force developers to find and fix cope that accessed the store from another thread. This was an improvement, but still cumbersome in many cases.
Now in ReduxKotlin version 0.5.1 has settled on #3: a synchronized, thread-safe store. This is now the recommended way to use ReduxKotlin on multi-threaded platforms. It also supports all platforms except linArm32 & wasm (due to AtomicFu restrictions)
To use, just update to:
val store = createThreadSafeStore(reducer, state)
The ReduxKotlin dependency will also need updating:
kotlin { sourceSets { commonMain { dependencies { implementation "org.reduxkotlin:redux-kotlin-threadsafe:0.5.1" } } } }
The new thread-safe store uses AtomicFu’s synchronization to allow multi-thread access to all the functions. It is simply a wrapper around the store’s functions. The store can now be access from any thread.
The same thread enforcement code has been moved out of the createStore
function now. It is still available to those who want it by using createSameThreadEnforcedStore.
Back to our failing test from above. Replacing createStore
with createThreadSafeStore
immediately fixes the test:
Perfomance
Adding synchronization does come with some performance overhead. From my experience and testing this has not been a problem. I have not seen any user visible effects of using the thread-safe store, but it something to keep in mind. A few findings:
- Calling
getState
was always well under < 1ms with or without synchronization dispatch
calls took 1-3ms longer with synchronization- During a cold launch of an Android app differences were more dramatic. Some calls to
dispatch
took +100ms more than a store that was not synchronized.
These are not very scientific tests. Perhaps I’ll get to proper benchmarking at some point.
Why a separate artifact
ReduxKotlin has always had the goal of staying close to JS Redux API and support all possible use cases of the library. This means ALL platforms. Some users of ReduxKotlin may target only javascript, in which case they don’t need thread safety (and don’t want the extra bloat of AtomicFu). The thread safe store requires the redux-kotlin-threadsafe
artifact. It is a very small module in the ReduxKotlin project.
The unit tests show above are available here.
Happy Coding!