[Kotlin] Coroutine Dispatchers 1편

H43RO·2021년 9월 3일
8

Kotlin 과 친해지기

목록 보기
11/18
post-thumbnail

💡 코틀린 공식 문서를 참고하여 작성한 글입니다 - Coroutines basics | Kotlin

이전 포스팅과 이어집니다! [Kotlin] Coroutine 취소하기

코루틴은 항상, 코틀린 표준 라이브러리에 정의되어 있는 CoroutineContext 타입의 특정 Context 안에서 실행되게 된다. Coroutine Context 는 다양한 요소로 이루어져있는데, 이를 이루는 대표적인 녀석들로는 우리가 이전에 몇 번 다루었던 Job 이랑, 이번 포스팅에서 알아보게 될 Dispatcher 등이 있다.

Dispatcher 가 뭐냐, 사전적 정의는 다음과 같다.

뭔가 어떤 체계적인 시스템을 통솔하고 지휘하는 듯한 느낌이 난다. 실제로 이번 포스팅에서 다뤄볼 Dispatcher 역시 이러한 역할을 하는게 맞다.

그럼 한 번, 어쩌면 생소할 수 있는 Dispatcher 개념에 대해 자세히 살펴보자.

Dispatchers and Threads

Coroutine Context 는 Coroutine Dispatcher 라는 녀석을 항상 포함하는데, 이는 해당 코루틴이 어떤 쓰레드 (혹은 쓰레드들) 위에서 실행되게 할지 명시해줄 수 있다. 즉, 코루틴의 실행을 특정 쓰레드에 국한시켜주거나, 특정 쓰레드 풀로 전달해주는 역할을 한다. (혹은 아무 제한 없이 실행되게 하거나) 위에서 사전적 정의는 '운행 관리원'이라고 했지 않았는가? 코루틴 세계의 관리원인 것이다.

launch 와 같은 Coroutine Builder 는 CoroutineContext 타입의 파라미터를 받는다. 물론 꼭 넘겨주지 않아도 된다. 이 파라미터는 새로운 코루틴Context 요소들을 어디서 실행 시킬지, Dispatcher 를 명시하는 용도로 쓰이게 된다. 즉 다양한 Dispatcher 가 존재한다는 뜻이다.

한 번 아래 코드를 실행해보고, 실행 결과를 보자. 각 코루틴이 어떤 쓰레드에서 동작되고 있는 지를 출력하여, 다양한 종류의 Dispatcher 가 코루틴을 어떻게 지휘하고 관리하는지 살펴보자.

fun main() = runBlocking<Unit> {
    launch {   // 인자를 안 넘겨주면 상위 코루틴의 Context 를 가짐
        println("main runBlocking       : 나는 ${Thread.currentThread().name} 에서 돌아")
    }
    launch(Dispatchers.Unconfined) {  // 별도로 지정을 안 해줌
        println("Unconfined             : 나는 ${Thread.currentThread().name} 에서 돌아")
    }
    launch(Dispatchers.Default) {  // DefaultDispatchers 등록
        println("Default                : 나는 ${Thread.currentThread().name} 에서 돌아")
    }
    launch(newSingleThreadContext("H43RO_Thread")) {  // 새로운 쓰레드 생성
        println("newSingleThreadContext : 나는 ${Thread.currentThread().name} 에서 돌아")
    }
}
Unconfined             : 나는 main 에서 돌아
Default                : 나는 DefaultDispatcher-worker-1 에서 돌아
main runBlocking       : 나는 main 에서 돌아
newSingleThreadContext : 나는 H43RO_Thread 에서 돌아

만약 처음 접해보는 개념이라면 실행결과가 의아할 것이다. 하나씩 뜯어보며 살펴보자!

인자를 안 넘겨줬을 때

launch {   // 인자를 안 넘겨주면 상위 코루틴의 Context 를 가짐
    println("main runBlocking       : 나는 ${Thread.currentThread().name} 에서 돌아")
}

아무 인자 없이 launch { } 를 사용하면, 이를 실행한 상위 CoroutineScope 로부터 Context (Dispatchers 포함) 를 상속받게 된다. 위 코드에서는 상위 CoroutienScopemain 쓰레드에서 돌아가고 있는 runBlocking 스코프이므로 해당 스코프의 Context 를 상속받게 된다. 따라서 main 쓰레드가 출력되는 것이다.


Dispatchers.Unconfined

launch(Dispatchers.Unconfined) {  // 지정을 안 해줌
    println("Unconfined             : 나는 ${Thread.currentThread().name} 에서 돌아")
}

아무 Context 를 지정하지 않겠다는 의미로 사용되는 Dispatchers.Unconfined 는 이를 호출한 쓰레드에서 동작되게끔 한다. 따라서 위 예제로 따지면 main 에서 동작하는 것처럼 보이긴하는데, 사실은 조금 다르다. 이후 정확한 메커니즘 차이를 설명하겠다. (곧 바로 짚고 넘어갈테니 너무 킹 받아 하지 않아도 된다)


Dispatchers.Default

launch(Dispatchers.Default) {  // DefaultDispatchers 등록
    println("Default                : 나는 ${Thread.currentThread().name} 에서 돌아")
}

Dispatchers.Default 는 코루틴이 GlobalScope 에서 사용되어 별다른 Dispatcher 명시가 필요없는 상황에 사용된다. 이것은 JVM 에서 제공되는 공용 Background 쓰레드 풀을 사용하게 된다.


newSingleThreadContext

launch(newSingleThreadContext("H43RO_Thread")) {  // 새로운 쓰레드 생성
    println("newSingleThreadContext : 나는 ${Thread.currentThread().name} 에서 돌아")
}

newSingleThreadContext()해당 코루틴이 돌아가게 될 새로운 쓰레드를 생성해준다. 그런데 이렇게 생성한 쓰레드는 분명 엄청난 고비용 자원일 것이다. 단지 이 코루틴을 위해 생성된 거니까. 따라서 실제 프로그램 개발 시에는 해당 쓰레드를 더이상 사용할 필요가 없다면 꼭 close 와 같은 함수를 사용하여 메모리에서 해제하고, 이를 최상위 변수에 저장해두어 다른 녀석들도 재사용할 수 있도록 해야 효율적이다.

아까 Dispatchers.Unconfined 에 대해서 설명하다 말았는데, 이제 한 번 자세히 살펴보자.


Unconfined vs Confined Dispatcher

Dispatchers.Unconfined 는 아까 말했듯이 해당 코루틴을 호출한 쓰레드에서 실행을 하게끔 해주는데, 사실은 이 쓰레드에서 실행을 시작해도 해당 코루틴의 첫 번째 중단점까지만 실행된다. 가장 큰 특징이다. (근본이 없다) 그냥 자기 불러주는 쓰레드 졸졸 따라가서 거기 위에서 실행되는 녀석이다.

중단점 이후 다시 재개될 땐 Suspending Function 가 재개된 (호출된) 쓰레드에서 재개된다. Unconfined Dispatcher 는 CPU 타임을 잡아 먹지 않고, 공유 데이터 (UI) 등을 건드리지 않는 등 특정 쓰레드에 국한되지 않고 동작하는 경우 사용하면 된다.

한편 Dispatcher (혹은 Context 요소들) 는 기본적으로 상위, 그러니까 부모 (바깥 스코프) CoroutineScope 로부터 상속된다. runBlocking 코루틴을 위한 기본 디스패처는, 이를 호출한 쓰레드에 국한되기 때문에 이를 상속하면 해당 쓰레드에 국한될 뿐더러 예측 가능한 형태의 FIFO 스케줄링으로 수행된다.

launch(Dispatchers.Unconfined) {
    println("Unconfined             : 나는 ${Thread.currentThread().name} 에서 돌아")
    delay(500)
    println("Unconfined             : 딜레이 이후에는 ${Thread.currentThread().name} 에서 돌아")
}
launch {
    println("main runBlocking       : 나는 ${Thread.currentThread().name} 에서 돌아")
    delay(1000)
    println("main runBlocking       : 딜레이 이후에는 ${Thread.currentThread().name} 에서 돌아")
}
Unconfined             : 나는 main 에서 돌아
main runBlocking       : 나는 main 에서 돌아
Unconfined             : 딜레이 이후에는 kotlinx.coroutines.DefaultExecutor 에서 돌아
main runBlocking       : 딜레이 이후에는 main 에서 돌아

runBlocking 녀석으로부터 상속받은 Context 로 동작하는 코루틴은 main 쓰레드에서 계속하여 진행되고, 반면 Unconfined 한 녀석은 DefaultExecutor 라는 쓰레드에서 동작이 재개된 것을 확인할 수 있다. 그러니까, 즉 DefaultExecutor 에서 delay() 가 호출된 것이다.

Unconfined Dispatcher 는 위험요소가 커 일반적인 경우 사용을 지양하라고 공식 문서에서 소개한다. 예를 들어 어떤 코루틴에서 갑자기 어떤 동작을 해야돼서 쓰레드 전환을 하고자 코루틴이 잠시 디스패치 되어 일부 동작이 나중에 실행되어 버리거나, 그리고 이렇게 본의 아니게 나중에 실행 되어버렸을 때 예상치 못한 사이드 이펙트를 만들어 내는 등 경우 매우 위험한 것이다. 따라서 일반적인 경우 Unconfined Dispatcher 를 사용하지 말자.


우선 생소한 Dispatchers 개념이 나올 뿐더러 다양한 Dispatcher 의 종류가 나왔기 때문에 어지러울 것이다. 이 포스팅에선 여기까지 익혀두고, 다음 포스팅에서 이어서 작성하겠다.

profile
어려울수록 기본에 미치고 열광하라

1개의 댓글

comment-user-thumbnail
2022년 5월 18일

2편도 써주세요!!

답글 달기