코루틴에는 어떤 스레드에서 돌아갈 지 정의하는 Dispatcher 가 있다. Dispatcher 는 Event Loop 를 통해 작동된다. Event Loop 란 Call Stack 과 Callback Queue 의 상태를 반복적으로 체크하며, 함수를 실행할 시간이 되면 Callback Queue 의 첫 번째 아이템을 Call Stack 으로 넣고 실행시키는 기술이다. 이러한 반복적인 행동을 틱(tick) 이라고 부른다.
Dispatcher 는 Default, Main, Unconfined, IO 이렇게 4가지가 있다. 이 중 Unconfined 를 제외한 나머지 3개들은 각각 해당하는 스레드를 기준으로 Call Stack 이 구성된다.
코루틴 스코프에서 함수를 실행하면 해당 함수가 Callback Queue 에 추가된다. 이후 해당 함수를 실행해야 할때 CoroutineDispatcher.isDispatchNeeded 를 통해 디스패치가 필요한지 확인하고, 디스패치가 필요하다면 해당 함수가 사용된 코루틴 스코프에서 사용하고 있는 Dispatcher 에 맞는 Call Stack 에서 함수를 실행한다.
만약 Dispatcher 가 Unconfined 라면 마지막으로 함수가 실행된 Call Stack 에서 해당 함수를 실행한다.
기본 형태인 Dispatchers.Main 으로 한다면 디스패치가 필요하기 때문에 위에서 알아본 원리처럼 Callback Queue 에 등록되고 이후 실행해야 할 시간이 됐다면 Dispatchers.Main 의 Call Stack 에서 함수가 실행되면서 비동기로 작동된다.
Dispatchers.Main.immediate 을 사용한다면 이미 해당 함수가 메인 스레드에 있다는 걸 의미하고, Main 으로 디스패치를 요구하지 않는다(이미 메인 스레드에 있기 때문에 추가적인 디스패치가 필요 없기 때문입니다). 따라서 해당 함수가 Callback Queue 에 등록되고 이후 Call Stack 에서 비동기로 실행되는게 아닌, 즉시 동기로 실행됩니다.
fun main() {
CoroutineScope(Dispatchers.Main).launch {
println(1)
}
println(2)
}
위 코드를 실행한다면 println(1) 이 디스패치가 필요하여 비동기로 작동되기 때문에 2 가 먼저 출력되고 이후 1 이 출력된다.
fun main() {
CoroutineScope(Dispatchers.Main.immediate).launch {
println(1)
}
println(2)
}
반면에 위 코드를 실행한다면 println(1) 이 immediate 이기 때문에 디스패치가 필요하지 않아 동기로 작동하기 때문에 1, 2 가 차례대로 출력된다.
이렇게 immediate 는 순서 보장과 최적화에 유용하게 쓰일 수 있다. immediate 는 이미 해당 함수가 해당 스레드에 있다고 가정하고 추가적인 디스패치를 요구하지 않는 Dispatcher 이기 때문에 모든 환경에서 기본 값으로 존재하는 메인 스레드에 대해서만 가능하다. 이런 이유로 Dispatchers.Main 이 아닌 다른 Dispatchers 에는 immediate 옵션이 존재하지 않는다.
Dispatchers.Main.immediate 는 viewModelScope 와 lifecycleScope 에 기본 값으로 사용되고 있다.
안드로이드는 기본 스레드가 메인 스레드 하나밖에 없기 때문에 기본 값으로 모든 함수들은 메인 스레드에서 실행되기 때문이다.