Thread
#multiplatform
    kpgalligan

    kpgalligan

    2 years ago
    Anybody use Stately collections? We’re starting to look at replacing them and want to better understand use cases.
    Erik Christensen

    Erik Christensen

    2 years ago
    Currently using the shared hash map in Island Time (native only) as the closest thing to a
    ConcurrentHashMap
    . I use it to share hashes between threads that are built up lazily. It's possible there's a better/more efficient approach with K/N, but the Stately shared hash map seemed easy/familiar enough to use for the time being.
    b

    basher

    2 years ago
    We use the frozen map and list types
    kpgalligan

    kpgalligan

    2 years ago
    Playing with using multithreaded coroutines to implement shared collections by keeping the “collection” in a single thread, and storing/pulling frozen data. Performs significantly better than Stately collections.
    b

    basher

    2 years ago
    Will that make stately-collections require kotlinx.coroutines?
    kpgalligan

    kpgalligan

    2 years ago
    Well, I think it’ll be a different module. That is the issue
    Dependency
    Erik Christensen

    Erik Christensen

    2 years ago
    Interesting finding. So even with the context switch, it's still faster to read and write to a single owning thread?
    kpgalligan

    kpgalligan

    2 years ago
    I’m going through the rest of Stately now. A lot of it is implemented by atomic fu
    Yeah, but even just doing that with a worker is faster. What’s way faster is if you do batch stuff.
    val imap = IsoMap<String, SomeData>(createState { mutableMapOf<String, SomeData>() })
            val duration = measureTime {
                repeat(100_000) {
                    imap.put("ttt $it", SomeData("jjj $it"))
                }
            }
    
            println("Coroutine duation ${duration} count ${imap.size()}")
    
            imap.clear()
    
            val coroutineBatchduration = measureTime {
                repeat(100) {outerLoop ->
                    val dataMap = mutableMapOf<String, SomeData>()
                    repeat(1000){
                        val index = (outerLoop * 1000) + it
                        dataMap.put("ttt $index", SomeData("jjj $index"))
                    }
                    imap.putMap(dataMap)
                }
            }
    
            println("Coroutine batch duation ${coroutineBatchduration} count ${imap.size()}")
    
            imap.clear()
    
            val m = mutableMapOf<String, SomeData>()
            val mutableDuration = measureTime {
                repeat(100_000) {
                    m.put("ttt $it", SomeData("jjj $it"))
                }
            }
    
            println("Mutable duation ${mutableDuration} count ${m.size}")
    
            val fm = frozenHashMap<String, SomeData>()
            val frozenDuration = measureTime {
                repeat(100_000) {
                    fm.put("ttt $it", SomeData("jjj $it"))
                }
            }
    
            println("Frozen duation ${frozenDuration} count ${fm.size}")
    The first 2 are using coroutines, the second is batching. The 3rd is just using mutable state, and the last is using stately
    Coroutine duation 18.4s count 100000
    Coroutine batch duation 634ms count 100000
    Mutable duation 451ms count 100000
    Frozen duation 30.5s count 100000
    So, stately is almost 2x worse for puts, but when using batches you can really speed things up. You have more flexibility. In the general case, you provide a lambda that takes the collection as an arg, and as long as you don’t try to return it, you can do what you want
    open class IsolateState<T : Any>(private val stateId: Int) {
        internal suspend fun <R> access(block: (T) -> R): R = withContext(stateDispatcher) {
            block(stateMap.get(stateId) as T)
        }
    }
    The map then just does whatever through
    access
    calls
    class IsoMap<K,V>(stateId: Int) : IsolateState<MutableMap<K,V>>(stateId){
        suspend fun clear() = withContext(stateDispatcher) {
            access { it.clear() }
        }
    
        suspend fun size() = withContext(stateDispatcher) {
            access { it.size }
        }
    
        suspend fun get(mapKey: K): V? = withContext(stateDispatcher) {
            access { it.get(mapKey) }
        }
    
        suspend fun put(mapKey: K, data: V) = withContext(stateDispatcher) {
            access { it.put(mapKey, data) }
        }
    
        suspend fun putMap(map: Map<K, V>) = withContext(stateDispatcher) {
            access { it.putAll(map) }
        }
    }
    I’m still working through complications, but performance and flexibility are pretty good. As for dependency on kotlinx.coroutines, I imagine most apps will eventually depend on that.
    b

    basher

    2 years ago
    I'd be curious to know what it is about coroutines that makes it faster
    kpgalligan

    kpgalligan

    2 years ago
    Nothing about coroutines specifically. It’s the mutable state in the single background thread. Could just do this directly with a worker, and may do that. I think playing with coroutines got me thinking.
    Erik Christensen

    Erik Christensen

    2 years ago
    I would think that using C interop to bypass the K/N rules would lead to the fastest implementation. Like what they talk about in the concurrency docs: https://github.com/JetBrains/kotlin-native/blob/master/CONCURRENCY.md#raw-shared-memory
    kpgalligan

    kpgalligan

    2 years ago
    Well, yeah. Don't disagree
    I started a c++ implementation. However, I'm less concerned about "fastest" than fast and flexible. Will be a general mutable state container
    Also, you could just set the state to "shared"
    Erik Christensen

    Erik Christensen

    2 years ago
    Haha... well, yes. The devil is in the implementation details though. It's a bit of work to really optimize it.
    In any case, current stately implementation is super slow
    A lot of stately is going to be replaced with atomic fu, so im kind of thinking now is a good time to rethink the whole thing
    Erik Christensen

    Erik Christensen

    2 years ago
    Seems fair enough.
    Qinghua

    Qinghua

    2 years ago
    Playing with using multithreaded coroutines to implement shared collections by keeping the “collection” in a single thread, and storing/pulling frozen data. Performs significantly better than Stately collections.
    It seems that using a Worker can achieve that read/write mutable shared collection in a single thread. Why it needs
    coroutine
    here?
    kpgalligan

    kpgalligan

    2 years ago
    "Nothing about coroutines specifically. It’s the mutable state in the single background thread. Could just do this directly with a worker, and may do that. I think playing with coroutines got me thinking." Said that above
    Qinghua

    Qinghua

    2 years ago
    Give a try using a worker as above demonstrated.
    Oh, maybe I see your intention in terms of coroutine.