코딩하는 개굴이

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

안드로이드/Coroutine

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

개굴이모자 2023. 5. 30. 01:08
반응형

Dispatchers.Main 을 이용해 로직을 짜는데, 뭔가 이상하게 순서가 꼬였다 싶은 순간이 있을 수 있다.

보통의 개발자는 위에서부터 아래로 코드의 실행 순서를 가정하고 개발을 하곤하기에 Dispatchers.Main 의 정확한 이해는 중요하다.

따라서, 순서가 보장되게 이를 사용하려면 어찌 해야하는지 한번 알아보도록 하자.

 

그러기 위해서 Dispatcher이 무엇인지 코루틴의 동작은 어찌되는지 먼저 확인해보자.

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