ATOMIC

Atomically (i.e., in a non-cancellable way) schedules the coroutine for execution according to its context.

This is similar to DEFAULT, but the coroutine is guaranteed to start executing even if it was cancelled. This only affects the behavior until the body of the coroutine starts executing; inside the body, cancellation will work as usual.

Like ATOMIC, UNDISPATCHED, too, ensures that coroutines will be started in any case. The difference is that, instead of immediately starting them on the same thread, ATOMIC performs the full dispatch procedure just as DEFAULT does.

Because of this, we can use ATOMIC in cases where we want to be certain that some code eventually runs and uses a specific dispatcher to do that.

Example:

val mutex = Mutex()

mutex.lock() // lock the mutex outside the coroutine
// ... // initial portion of the work, protected by the mutex
val job = launch(start = CoroutineStart.ATOMIC) {
// the work must continue in a coroutine, but still under the mutex
println("Coroutine running!")
try {
// this `try` block will be entered in any case because of ATOMIC
println("Starting task...")
delay(10.milliseconds) // throws due to cancellation
println("Finished task.")
} finally {
mutex.unlock() // correctly release the mutex
}
}

job.cancelAndJoin() // we immediately cancel the coroutine.
mutex.withLock {
println("The lock has been returned correctly!")
}

Here, we used ATOMIC to ensure that a mutex that was acquired outside the coroutine does get released even if cancellation happens between lock() and launch. As a result, the mutex will always be released.

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

// Example of cancelling atomically started coroutines
runBlocking {
println("1. Atomically starting a coroutine that goes through a dispatch.")
launch(start = CoroutineStart.ATOMIC) {
check(!isActive) // attempting to suspend later will throw
println("4. The coroutine was cancelled (isActive = $isActive), but starts anyway.")
try {
delay(10.milliseconds) // will throw: the coroutine is cancelled
println("This code will never run.")
} catch (e: CancellationException) {
println("5. Cancellation at later points still works.")
throw e
}
}
println("2. Cancelling this coroutine and all of its children.")
cancel()
launch(Dispatchers.Unconfined, start = CoroutineStart.ATOMIC) {
check(!isActive) // attempting to suspend will throw
println("3. An undispatched coroutine starts.")
}
ensureActive() // we can even crash the current coroutine.
}

This is a delicate API. The coroutine starts execution even if its Job is cancelled before starting. However, the resources used within a coroutine may rely on the cancellation mechanism, and cannot be used after the Job cancellation. For instance, in Android development, updating a UI element is not allowed if the coroutine's scope, which is tied to the element's lifecycle, has been cancelled.