Kotlinx.coroutines: Android: Main.immediate causes a dispatch using Unconfined inside

Created on 7 Nov 2019  路  6Comments  路  Source: Kotlin/kotlinx.coroutines

Dispatchers.Main.immediate forces child coroutine with Dispatchers.Unconfined interceptor to dispatch.

Example code

GlobalScope.launch(Dispatchers.Main.immediate) {
    GlobalScope.launch(Dispatchers.Unconfined) {
        println("$coroutineContext: before")
    }
    println("$coroutineContext: after")
}

This code prints

[CoroutineId(1), "coroutine#1":StandaloneCoroutine{Active}@229841e, Main [immediate]]: after
[CoroutineId(2), "coroutine#2":StandaloneCoroutine{Active}@f2294ff, Unconfined]: before

But expected would be backwards.

I can force not to dispatch if I use start = CoroutineStart.UNDISPATCHED. Then I get the expected result

[CoroutineId(4), "coroutine#4":StandaloneCoroutine{Active}@229841e, Unconfined]: before
[CoroutineId(3), "coroutine#3":StandaloneCoroutine{Active}@f2294ff, Main [immediate]]: after

But shouldn't GlobalScope.launch(Dispatchers.Unconfined) and GlobalScope.launch(start = CoroutineStart.UNDISPATCHED, context = Dispatchers.Unconfined) be interchangable?

docs question

Most helpful comment

It is a mix of "immediate" interaction with channel unfairness (#111).
Two solutions here:
1) Yielding after send to make sending to the channel fair
2) Launching sender on main dispatcher and receiver on immediate one:

GlobalScope.launch(Dispatchers.Main) {
    val c = Channel<Int>()
    launch(Dispatchers.Main.immediate) {
        c.consumeAsFlow()
                .collect {
                    println(it)
                }
    }
    launch {
        c.send(1)
        c.send(2)
   }
}

No way to force this behaviour on the level of a dispatcher, because it's easy to make a mistake here e.g. during code evolution, not at the moment of writing, thus we don't endorse this.
In theory, a sophisticated user can write its own dispatcher around Dispatchers.Main that behaves
in a way you want, but we neither advocate it nor have an extensive test suite for such dispatchers (thus it may break in weird ways)

All 6 comments

By the way using Dispatchers.Main yield correct result

[CoroutineId(29), "coroutine#29":StandaloneCoroutine{Active}@c6ed5c2, Unconfined]: before
[CoroutineId(25), "coroutine#25":StandaloneCoroutine{Active}@65e4fd3, Main]: after

This is a protection to avoid spurious stack overflows when you have (usually implicit) nested unconfined coroutines. Otherwise, the simplest ping-pong with a channel and two unconfined coroutines would fail with StackOverflowError.

This behaviour reflected both in Unconfined and immediate dispatchers documentation:

Immediate dispatcher is safe from stack overflows and in case of nested invocations forms event-loop similar to [Dispatchers.Unconfined].
The event loop is an advanced topic and its implications can be found in [Dispatchers.Unconfined] documentation.

Nested coroutines launched in this dispatcher form an event-loop to avoid stack overflows.
... long description of what event loop is and its implications ...

Well I didn't think that immediate and Unconfined share the same loop in the same thread but it perfectly makes sense. Otherwise it would not work.

I think the documentation to CoroutineStart.UNDISPATCHED is a bit misleading:

Immediately executes the coroutine until its first suspension point _in the current thread_ as if the coroutine was started using [Dispatchers.Unconfined].

The part about "as if the coroutine was started using [Dispatchers.Unconfined]" seems false when we are dealing with nested unconfined coroutines.

Good point, I will adjust our documentation accordingly

Btw is there a way to force a non-dispatch in cases where it is known that no stack overflow can occur (I mean within reasonable limits). For example, if I have two coroutines but the data flows only one way. In that case dispatch is superfluous (and can create data races). Look at the following example:

GlobalScope.launch(Dispatchers.Main.immediate) {
    val c = Channel<Int>()
    launch {
        c.consumeAsFlow()
                .collect {
                    println(it)
                }
    }
    launch {
        c.send(1)
        c.send(2)
    }
}

I would like to force such statement execution order:

c.send(1)
println(1)
c.send(2)
println(2)

Instead I always get

c.send(1)
c.send(2)
println(1)
println(2)

It is a mix of "immediate" interaction with channel unfairness (#111).
Two solutions here:
1) Yielding after send to make sending to the channel fair
2) Launching sender on main dispatcher and receiver on immediate one:

GlobalScope.launch(Dispatchers.Main) {
    val c = Channel<Int>()
    launch(Dispatchers.Main.immediate) {
        c.consumeAsFlow()
                .collect {
                    println(it)
                }
    }
    launch {
        c.send(1)
        c.send(2)
   }
}

No way to force this behaviour on the level of a dispatcher, because it's easy to make a mistake here e.g. during code evolution, not at the moment of writing, thus we don't endorse this.
In theory, a sophisticated user can write its own dispatcher around Dispatchers.Main that behaves
in a way you want, but we neither advocate it nor have an extensive test suite for such dispatchers (thus it may break in weird ways)

Was this page helpful?
0 / 5 - 0 ratings