https://kotlinlang.org logo
#coroutines
Title
# coroutines
l

Leon K

02/21/2020, 2:18 PM
Hey! I have a variable that I need Synchronized access to from two functions: a
suspend
fun and a non-suspending function. I cannot just do
@Synchronized
on the blocking one and use
Mutex
on the suspend-one as these don't exclude each other, such that there is still the posibility of two concurrent accesses. It seems as tough
Mutex
is not usable from non-blocking code, and
@Synchronized
doesn't help with `suspend fun`s. what's the best approach here?
d

diesieben07

02/21/2020, 2:44 PM
You could probably use
Mutex
inside
runBlocking
to achieve an effect similar to
@Synchronized
(block instead of suspend when waiting for the lock).
l

Leon K

02/21/2020, 2:45 PM
Ahhh, that sounds like a great solution! thanks!
u

uli

02/21/2020, 2:47 PM
But maybe you should rather consider, why you have this problem. To me it sounds like a first hint at a design issue.
l

Leon K

02/21/2020, 2:48 PM
I have a pretty generic, very simple "single-value" cache, that should be usable from coroutine-stuff as well as non-suspending context. I could propably try to separate this into two classes or maybe make the blocking context non-blocking too, but that would just move the place of
runBlocking
to somehwere else...
z

Zach Klippenstein (he/him) [MOD]

02/21/2020, 2:53 PM
Do you even need a lock? If you're just implementing a cache you can probably get away with something like an
AtomicReference
and not deal with locking at all.
l

Leon K

02/21/2020, 2:54 PM
that seems like an option too, good idea. I've never used AtomicReference, I'll look into it ;D
is it possible to give
AtomicReference
a
suspend (T) -> T
? otherwise this would only solve my non-suspending case, which is only half my use-case ;D
z

Zach Klippenstein (he/him) [MOD]

02/21/2020, 2:59 PM
Why do you need to suspend/block at all? Are you trying ensure the cache is initialized only the first time it's read?
a

Arkadii Ivanov

02/21/2020, 3:01 PM
You could use
synchronized {}
block. Just don't suspend inside the block.
l

Leon K

02/21/2020, 3:02 PM
So, I'll tell you my use-case: I have some data that becomes out-of-date quickly, but loading it is still "somewhat" expensive, and i want to cache the result. This is the class:
Copy code
class ExpirationBasedCache<V>(private val expirationTimeSeconds: Long) {
    @Volatile
    private var cacheData: Pair<Long, V>? = null
    private val mutex = Mutex()

    /** Read the value from the cache if it exists and is not expired,
     * otherwise calculate a new value using the given [function][f]. */
    fun getValueBy(f: suspend () -> V): V = runBlocking { getValueBySuspending(f) }

    /** Read the value from the cache if it exists and is not expired,
     * otherwise calculate a new value using the given [function][f]. */
    suspend fun getValueBySuspending(f: suspend () -> V): V = mutex.withLock {
        cacheData
            ?.takeIf { (timeOfCache, _) -> System.currentTimeMillis() - timeOfCache < expirationTimeSeconds * 1000 }
            ?.second
            ?: f().also { cacheData = Pair(System.currentTimeMillis(), it) }
    }
}
because this cache should be accessible from both suspending and non-suspending contexts, I need to support both. The idea is that, if one thread is currently calculating the value, others don't start doing the same thing but wait for the first thread and then reuse it's result.
thus for this case,
synchronized
is not an option as i DO need to suspend within the block
a

Arkadii Ivanov

02/21/2020, 3:10 PM
Then I would go with runBlocking. I would define two functions:
supend fun get()
and
fun getBlocking()
The last one will call the first under runBlocking block. And use Mutex.
l

Leon K

02/21/2020, 3:11 PM
that's exactly what I'm doing now, if you read the code ;D Thanks for the help everyone, didn't think about runBlocking! (I tried to forget it exists to avoid using it where it isn't appropriate haha)
2 Views