Michael
04/17/2025, 8:58 PMlimitedParallelism(1)
pattern, which, according to the documentation, guarantees sequential task execution and establishes a happens-before relationship between them. So it makes shared state thread-safe even when accessed from different coroutines.
So, I was wondering:
• Is this something that the LimitedDispatcher
explicitly enforces, or is it just a side effect of the lack of parallelism? In other words, if I don’t use limitedParallelism
, but it just so happens that two different coroutines on the same dispatcher are executed sequentially, does that mean one coroutine is guaranteed to see modifications made by the other?
• I’ve been struggling to reproduce any memory visibility issues.. It seems that once a coroutine suspends, any other coroutine can immediately observe changes made to shared state (without additional synchronization). Am I just lucky, or are there some subtleties in how this works that you could give insight into?
Thanks in advance 😊rkechols
04/17/2025, 11:26 PMsynchronization
. My understanding is that your linked article uses the term to refer to an abstract system that ensures one thread waits for another thread. For example if you use an explicit lock, semaphore, etc. to ensure that some mutable shared state is only mutated by one thread at a time, you've built "synchronization" around the shared state. Java has the synchronized
keyword to make this pattern easier (docs).
Regarding Kotlin, take this example:
suspend fun parent() {
child1()
child2()
}
suspend fun child1() { println("child 1") }
suspend fun child2() { println("child 2") }
When that article has the code example with yield()
and mentions that Kotlin does synchronization, it just means that Kotlin won't let child2
start until child1
is done (child1 "happens before" child2), even though we're in a context of concurrency and they may actually run on different threads.
If you have shared mutable state visible to multiple coroutines or threads, there's just the one place in memory where that is defined, mutated, etc. "synchronization" is not referring to some copying of shared state between threads, or anything like that. It just means we prevent two pieces of code from touching something at the same time or in an unpredictable orderrkechols
04/17/2025, 11:50 PMlimitedParallelism(1)
gives thread-safety purely by nature of only letting one thread run at a time. If only one thread is allowed to run at a time, then you know you'll never have 2 that are mutating the same shared state in the same instant.
If you have two coroutines and you somehow have the guarantee that one can only start after the other finishes, you are also indeed guaranteed that state mutations made by the first will be visible to the second.
No, you're not just lucky. Shared state is shared state... There's no sneaky duplication going on behind the scenes.CLOVIS
04/18/2025, 8:08 AMMichael
04/18/2025, 5:54 PMlimitedParallelism
.
I’d like to better understand how coroutines synchronize under the hood. Could it be that suspension itself establishes a happens-before relationship even across different coroutines (for example, if they’re using the same dispatcher)?CLOVIS
04/18/2025, 6:03 PMI’d like to better understand how coroutines synchronize under the hood. Could it be that suspension itself establishes a happens-before relationship even across different coroutines (for example, if they’re using the same dispatcher)?I'm not an expert on this topic, since as I mentioned the point of coroutines is to avoid shared state. However; • You can create explicit happens-before through
Mutex
and Semaphore
• (for example, if they’re using the same dispatcher) → not for any dispatchers, no. Single-threaded dispatchers yes, but I don't think there are any such guarantees for other kinds of dispatchers.Dmitry Khalanskiy [JB]
04/22/2025, 7:56 AMDispatchers.Default
or <http://Dispatchers.IO|Dispatchers.IO>
can access the same state in parallel, without establishing happens-before. But in one coroutine, even when it switches between threads in the course of its execution, [the part before] the suspension happens-before [the part after] the resumption.Michael
04/22/2025, 8:17 AMDispatchers.Default
or <http://Dispatchers.IO|Dispatchers.IO>
can run in parallel and access shared state concurrently.
That said, I was asking something else. I'm specifically trying to understand what suspension does under the hood in terms of memory visibility. Does suspension (perhaps unintentionally) establish a happens-before relationship between some coroutines? For example, if I can prove that one coroutine was launched after another coroutine suspended, is it guaranteed that it will observe the changes made by the first, even without any additional synchronization?Dmitry Khalanskiy [JB]
04/22/2025, 8:19 AMMichael
04/22/2025, 8:31 AMvar sharedBoolean = true
fun test() {
coroutineScope.launch(default) {
sharedBoolean = false
delay(1000)
}
coroutineScope.launch(default) {
delay(1000)
if (sharedBoolean) {
fail("oh no")
}
}
}
Dmitry Khalanskiy [JB]
04/22/2025, 8:33 AMDmitry Khalanskiy [JB]
04/22/2025, 8:37 AMsuspend
function's execution, its continuation object is passed to someone else (maybe simply to its dispatcher, maybe to someone who will resume it as a callback), and that is done in a manner that establishes the happens-before relationship.Michael
04/25/2025, 8:55 PMIs this (thread-safety) something that theIt's LimitedDispatcher. It has an internal "queue" that establishes a happens-before relationship when dispatching tasks ifexplicitly enforces, or is it just a side effect of the lack of parallelism?LimitedDispatcher
limitedParallelism = 1
I’ve been struggling to reproduce any memory visibility issues.. Am I just lucky?Yes. If you don't use any synchronization, single-threaded dispatchers, or limited parallelism = 1, you can encounter the same memory visibility issues as in Java. Suspension itself does not create a happens-before relationship
rakeeb
04/29/2025, 3:47 PMLimitedDispatcher
, I believe its still worth mentioning this caveat with you.
limitedParallelism(1)
is that its able to serialize non-suspending code. for e.g., if you have a function that delays (which is suspending):
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(10) { id ->
launch { demo(id + 1) }
}
joinAll()
}
val confinedDispatcher = Dispatchers.IO.limitedParallelism(1)
var counter = 0
suspend fun demo(id: Int) = withContext(confinedDispatcher) {
val duration = (10..100).random()
println(">>>> Enter: $id; counter value: $counter; delay value: $duration")
delay(duration.toLong())
counter++
println(">>>> Exit: $id; counter value: $counter")
}
delay
frees up the dispatcher to process other coroutines. if you run this code, while counter
will print out 10 at the end of the program, the counter
value observed by each coroutine in demo
looks out of order.