코딩하는 개굴이

[Kotlin] Coroutine Dispatchers.Main 의 동작 순서 보장 (feat. Dispatchers.Main.immediate) 본문

안드로이드/KOTLIN

[Kotlin] Coroutine Dispatchers.Main 의 동작 순서 보장 (feat. Dispatchers.Main.immediate)

개굴이모자 2023. 6. 18. 22:44
반응형

Dispatcher 란?

코루틴의 어떤 스레드에서 돌아갈지 정의하는 역할로써, Event Loop 를 통해 작동된다. Event Loop는 Call Stack 과 Callback Queue 의 상태를 반복적으로 체크하고, 함수를 실행할 시간이 되면 큐의 첫번째 아이템을 Call Stack 에 넣고 실행한다. 이렇게 반복하는데 이것을 tick 이라고 한다.

Call Stack 이 구성되는 Dispatcher 는 아래 3가지가 있다. 각각 해당하는 스레드를 기준으로 콜스택을 구성하게 된다.

  • Dispatchers.Main : UI를 구성하는 작업이 모여있는 쓰레드 풀
  • Dispatchers.IO : (파일 혹은 소켓을) 읽고 쓰는 작업이 모여있는 쓰레드 풀
  • Dispatchers.Default : 기본 쓰레드 풀, CPU 사용량이 많은 작업에 적합
  • (Dispatchers.Unconfined 의 경우, 콜스택을 구성하지 않는다.)

코루틴 스코프에서 함수를 실행하면 해당 함수가 Callback Queue 에 추가된다. 이후 해당 함수를 실행할 때, CoroutineDispatchers.isDispatchNeeded 를 통해 Dispatch 가 필요한지 확인하고 필요하다면 해당 함수가 사용된 스코프에서 사용하고 있는 Dispatcher 에 맞는 콜스택에서 함수를 실행한다. (Unconfined 의 경우 마지막으로 함수가 실행된 콜스택에서 해당 함수를 실행한다.)

자, 여기까지 왔으면 본론으로 들어가보자.

Dispatchers.Main 이 포함된 함수의 순서

아래처럼 특정 함수에 Dispatchers.Main 이 포함되어있다고 가정하자.

아래를 실행한다면 어떤 결과가 나오게 될까?

fun main() {
    println("first")
    CoroutineScope(Dispatchers.Main).launch {
        println("second")
    }
    println("third")
}

//결과
// first
// third
// second

메인 스레드에 실행시켰기 때문에 first-second-third 를 예상했을 수 있다. 그렇지만, 위에서 Dispatcher 의 동작 원리를 알아보았듯, 블록에 닿으면 Callback Queue 에 등록하고 tick 이 돈 이후에 Call Stack 에서 비동기로 실행되기 때문에 first-third-second 의 결과가 나오는 것이다.

그렇지만 비동기로는 실행해야하고 순서 또한 보장이 되어야한다면 어떻게 처리해야할까?

그럴 때, Dispatchers.Main.immediate 를 사용할 수 있다.

Dispatchers.Main.immediate

아까와 같은 코드에서 Dispatchers.Main.immediate 를 사용해보도록 하자.

fun main() {
    println("first")
    CoroutineScope(Dispatchers.Main.immediate).launch {
        println("second")
    }
    println("third")
}

//결과
// first
// second
// third

원하는 결과가 나왔다. 왜일까? Dispatchers.Main.immediate 를 사용한다는건 이미 해당 함수가 메인 스레드에 있다는 것을 의미하고, Main 으로 Dispatch 를 요구하지 않는다. 따라서, 해당 함수가 Callback Queue 에 등록되고 CallStack 에서 비동기로 실행되는게 아니라 즉시 동기로 실행이 되어버린다.

immediate 는 이미 함수가 특정 스레드에 있다고 가정하고 추가적인 Dispatch 를 요구하지 않기 때문에 메인스레드에서만 가능하다. 따라서, 순서 보장과 최적화에 유용하게 사용될 수 있으므로 viewModelScope 와 lifecycleScope 에 기본 값으로 사용되고 있음을 볼 수 있다.

public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }
    
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

+) withContext

  • CoroutineContext 를 변경할 때 사용하며, dispatcher 가 지정되면 해당 블럭을 다른 스레드로 이동하고 완료되면 원래 Dispatcher 로 돌아온다. (원래 스레드로 돌아오는 것은 아니고 thread pool 에서 알아서 관리해준다.)
  • suspend fun 혹은 코루틴 블럭 안에서 사용되어야한다.
  • async 와 동일한 역할을 하나, await 을 호출할 필요 없이 결과가 리턴될 때까지 기다린다.

참고 링크

반응형
Comments