https://kotlinlang.org logo
Title
m

Mihai Voicescu

10/26/2021, 6:43 PM
This might sounds like a very stupid set of questions but here goes (new to Kotlin)... Creating an actor delegate to synchronise access to an internal mutable state seems very easy, check at https://pl.kotl.in/Jztiy0l35 or below 1. This seems too easy, did I do something stupid (any performance pitfalls)? 2. If this is correct, why isn't something similar offered already in the standard lib? 3. In my opinion this should also work on Multiplatform(Native) (the actor API needs to be replace, but still). Am I wrong?
@OptIn(ObsoleteCoroutinesApi::class)
class ActorDelegate<T>(coroutineScope: CoroutineScope, create: () -> T) {
    class Package<T, R>(val block: T.() -> R) {
        val def = CompletableDeferred<R>()
    }

    private val channel = coroutineScope.actor<Package<T, Any>> {
        val t = create()

        channel.consumeEach {
            it.def.complete(it.block(t))
        }
    }

    suspend fun <R> withContext(block: T.() -> R): R {
        val pkg = Package(block)

        channel.send((pkg as Package<T, Any>))

        val r = pkg.def.await()

        return r as R
    }
}

fun <T> CoroutineScope.actorDelegate(create: () -> T) = ActorDelegate<T>(this, create)

class CoroutineMutableList(coroutineScope: CoroutineScope) {
    private val actorDelegate = coroutineScope.actorDelegate { mutableListOf<Int>() }


    suspend fun size() = actorDelegate.withContext { size }
    suspend fun contains(element: Int) = actorDelegate.withContext { contains(element) }
    suspend fun get(element: Int) = actorDelegate.withContext { get(element) }
    suspend fun add(element: Int) = actorDelegate.withContext { add(element) }
    // rest...
}
a

Andreas Scheja

10/26/2021, 7:48 PM
Well, first this is not exactly a delegate, delegates are usually referred by with
by
at the usage site and as such would have to at least provide a
operator
*fun* getValue(thisRef: Any?, property: KProperty<*>): T
method, which would already defeat your usage scenario as I don't think this it will allow the
suspend
keyword on that method and you won't be able to refer to
actorDelegate
as a type of
ActorDelegate<T>
but a type of
T
in your specific case it would probably be the simplest to just have a
private val lock = kotlinx.coroutines.sync.Mutex()
property and then use
lock.withLock { <code> }
instead of
actorDelegate.withContext { <code> }
m

Mihai Voicescu

10/27/2021, 8:44 AM
It would be simpler, yes. Unfortunately I need a global MutableMap to retrieve resources of connected users. This changes constantly and is very read intensive, so it will probably be the bottleneck of the application if it's not performant enough. As stated in the docs
Actor is more efficient than locking under load, because in this case it always has work to do and it does not have to switch to a different context at all.
A better way of doing it would probably be a ReadWriteLock. Unfortunately there is no ReadWriteLock in coroutines implemented and I don't have the time to implement it now 😢. Another alternative (which may be better depending on the number of disconnects) is an immutable AtomicReference<Map> that is rebuilt and replaced each time >1 user disconnects.
a

Andreas Scheja

10/27/2021, 9:24 AM
Yeah, I usually end up using actors too, but with a much more coarse grained interface, i.e. something like
suspend fun getUserData(id: Int): UserData
, as with your current approach if you ask for
size()
or
contains()
and then try to
get()
something from it another coroutine could have modified the map already, leading to undefined behaviour at best. In my classes I then use a
sealed interface
to distinguish between these coarser methods with a
when
inside the actor's
for
loop