UNDISPATCHED

Immediately executes the coroutine until its first suspension point in the current thread.

Starting a coroutine using UNDISPATCHED is similar to using Dispatchers.Unconfined with DEFAULT, except:

  • Resumptions from later suspensions will properly use the actual dispatcher from the coroutine's context. Only the code until the first suspension point will be executed immediately.

  • Even if the coroutine was cancelled already, its code will still start running, similar to ATOMIC.

  • The coroutine will not form an event loop. See Dispatchers.Unconfined for an explanation of event loops.

This set of behaviors makes UNDISPATCHED well-suited for cases where the coroutine has a distinct initialization phase whose side effects we want to rely on later.

Example:

var tasks = 0
repeat(3) {
launch(start = CoroutineStart.UNDISPATCHED) {
tasks++
try {
println("Waiting for a reply...")
delay(50.milliseconds)
println("Got a reply!")
} finally {
tasks--
}
}
}
// Because of UNDISPATCHED,
// we know that the tasks already ran to their first suspension point,
// so this number is non-zero initially.
while (tasks 0) {
println("currently active: $tasks")
delay(10.milliseconds)
}

Here, we implement a publisher-subscriber interaction, where UNDISPATCHED ensures that the subscribers do get registered before the publisher first checks if it can stop emitting values due to the lack of subscribers.

// Constant usage of stack space
fun CoroutineScope.factorialWithUnconfined(n: Int): Deferred<Int> =
async(Dispatchers.Unconfined) {
if (n 0) {
n * factorialWithUnconfined(n - 1).await()
} else {
1 // replace with `error()` to see the stacktrace
}
}

// Linearly increasing usage of stack space
fun CoroutineScope.factorialWithUndispatched(n: Int): Deferred<Int> =
async(start = CoroutineStart.UNDISPATCHED) {
if (n 0) {
n * factorialWithUndispatched(n - 1).await()
} else {
1 // replace with `error()` to see the stacktrace
}
}

Calling factorialWithUnconfined from this example will result in a constant-size stack, whereas factorialWithUndispatched will lead to n recursively nested calls, resulting in a stack overflow for large values of n.

The behavior of UNDISPATCHED can be described with the following examples:

runBlocking {
println("1. About to start a new coroutine.")
launch(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) {
println("2. The coroutine is immediately started in the same thread.")
delay(10.milliseconds)
println("4. The execution continues in a Dispatchers.Default thread.")
}
println("3. Execution of the outer coroutine only continues later.")
}
// Cancellation does not prevent the coroutine from being started
runBlocking {
println("1. First, we cancel this scope.")
cancel()
println("2. Now, we start a new UNDISPATCHED child.")
launch(start = CoroutineStart.UNDISPATCHED) {
check(!isActive) // the child is already cancelled
println("3. We entered the coroutine despite being cancelled.")
}
println("4. Execution of the outer coroutine only continues later.")
}

Pitfall: unlike Dispatchers.Unconfined and MainCoroutineDispatcher.immediate, nested undispatched coroutines do not form an event loop that otherwise prevents potential stack overflow in case of unlimited nesting. This property is necessary for the use case of guaranteed initialization, but may be undesirable in other cases. See Dispatchers.Unconfined for an explanation of event loops.