Is it safe to modify local mutable, non thread saf...
# coroutines
j
Is it safe to modify local mutable, non thread safe data structures in a coroutine that jumps between threads? e.g.
Copy code
fun main() = runBlocking<Unit> {
    val a = newSingleThreadContext("a")
    val b = newSingleThreadContext("b")
    launch {
        val set = mutableSetOf<Int>()
        repeat(100000) {
            withContext(a) {
                set.add(it)
            }
            withContext(b) {
                set.remove(it)
            }
        }
        println(set.size)
    }
}
I'd have thought it wouldn't be since set is being modified on multiple threads and isn't thread safe. On the other hand I can't see anything in the docs warning me not to do this and I haven't managed to produce any errors by doing seemingly unsafe operations.
c
My guess is that your code snippet isn’t actually running in parallel like you’re expecting. The entire
launch { }
block is running async, but each
withContext()
call is still run sequentially, and each iteration of the
repeat
block is also run sequentially. That’s the nature of coroutines, sequentially-async code. You’d have to
launch { }
or
async { }
each iteration of the loop for it to actually be executed in parallel, and I’d guess you would start seeing concurrent modification errors then
j
It's not that I expect it to run in parallel, but my understanding is that even if this is run sequentially it's not safe as set can be modified in one thread and the other thread may not see the changes even if it's executing afterwards unless the set is using volatile internally (which I don't think it is)
c
Yes, the default
mutableSetOf
(
LinkedHashSet
) is not thread-safe, and so your snippet is technically not safe. But it doesn’t keep checks of which threads are adding/removing elements from it, and as far as the Set is concerned, there are no threading issues since at one point in time only one thread is doing anything to it (since it’s not running in parallel). Each add/remove operation is synchronous, so it will not get in a point where an add is half-done by the time the next thread goes to remove it, since the Coroutines ensure the two
withContext
blocks are run sequentially, not in parallel
the first
withContext
block will always finish to completion before the second one ever starts, unless you
launch
or
async
it.
So to answer your original question, yes, it’s usually fine to modify non-synchronized data structures from coroutines, even ones that jump between threads, because the coroutines guarantee sequential and atomic access to that data structure at any given unit of time. Basically, if a given suspend function returns a value (instead of something like a
Deferred
or
Job
), you have a guarantee of this. This is actually one of the benefits of using coroutines, being able to use lock-free data structures in these situations
a
Actually withContext is kinda same(not exactly, it has some optimisatuin) as async{}.await() in order to understand its behavior it starts a new coroutine and then awaits for its completion, and the code is suspended it moves to the next line if and only if the withContext has been completed. And yesh main thread is suspended to do another tasks which may have been launched using launch or async.
But if you instead use async(a){} or b then there maybe concurrent addition and or removal or even remove could execute before add so there maybe some problem occur so...
j
Thanks for the replies. I guess what I was concerned about then was not atomicity or synchronicity but visibility, i.e. the changes made to the structure being written to main memory in case execution moves to a different core with its own cache. Does the coroutine guarantee that the next line after a context change won't start executing until the previous withContext block has written changes to its data structures to main memory?