IT/Kotlin

Coroutine Context and Dispatchers

물통꿀꿀이 2020. 11. 22. 12:56

여타 자원들 처럼 코루틴 또한 내부적으로 Context를 가지고 있다. 이를 코루틴 Context라 명명하는데, 실제 관련 문서를 살펴보면,

kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/

 

CoroutineContext - Kotlin Programming Language

 

kotlinlang.org

인덱싱된 Element 인스턴스의 집합으로 Persistent 속성을 가지고 있다.

내부적으로 코루틴 dispatcher를 가지고 있는데, 이후에도 계속 살펴보겠지만 dispatcher는 코루틴이 어떤 스레드에서 실행될지 결정해주는 필수 요소이다.

 

Dispatcher

간단하게 코루틴을 사용하게 되면 dispatcher에 신경을 쓰지 않아도 된다. (디폴트 값을 사용하면 되기 때문에)

예를 들어, launch, async 코루틴 빌더는 CoroutineContext 값이 optional이다. 그럼 좀 더 자세히 살펴보기 위해 코드를 보자.

fun main() = runBlocking<Unit> {
    launch { // context of the parent, main runBlocking coroutine
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher 
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }    
}
Unconfined : I'm working in thread main @coroutine#3
Default : I'm working in thread DefaultDispatcher-worker-1 @coroutine#4
newSingleThreadContext: I'm working in thread MyOwnThread @coroutine#5
main runBlocking : I'm working in thread main @coroutine#2

하나씩 살펴보자.

1) launch에 파라미터 값이 없을 때,

launch를 실행시킨 호출자의 CoroutineScope를 그대로 상속받는다. 위 코드에서의 base는 runBlocking이며 main thread에서 동작하는 코루틴이다. 때문에 해당 context의 scope는 main thread가 된다.

 

2) Dispatchers.Unconfined 일 때,

Unconfined은 말 그대로 어느 한 스레드에게 국한되지 않는다. 이해가 잘 되지 않을 수 있는데, 코루틴을 unconfined로 설정하게 되면 초기엔 코루틴을 호출한 스레드에 속해있다. 하지만 일시 정지 후 다시 시작하게 될 때, 다시 시작하게 하는 코루틴의 스레드에 속하게 된다.

즉, 자신을 호출한 스레드에 달라붙는다. (기x충??)

launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
    println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
    delay(500)
    println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
}
launch { // context of the parent, main runBlocking coroutine
    println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
    delay(1000)
    println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
}
Unconfined : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main

그러나 보기엔 좋아보이나 resume와 suspend의 연속인 코루틴의 특성상 사이드가 발생할 수 있는 여지가 크고, 공식 문서에서도 사용 자제를 당부한다고 쓰여있다. (아래 참조)

The unconfined dispatcher is an advanced mechanism that can be helpful in certain corner cases where dispatching of a coroutine for its execution later is not needed or produces undesirable side-effects, because some operation in a coroutine must be performed right away. The unconfined dispatcher should not be used in general code.

3) Dispatchers.Default 일 때,

해당 값은 스레드의 공유 자원을 사용한다. 즉, JVM에 의해 제공된 shared pool을 사용한다.

 

4) newSingleThreadContext 일 때,

새로운 스레드를 만들어 코루틴을 실행시킨다. 하지만 스레드를 만드는 것은 나름 일정 자원을 사용하는 것이기 때문에, 실제 환경에서는 종료후에 release가 필수로 이루어져야 한다.

Jumping between threads

withContext를 사용하면 스레드 간 전환이 가능해진다.

newSingleThreadContext("Ctx1").use { ctx1 ->
    newSingleThreadContext("Ctx2").use { ctx2 ->
        runBlocking(ctx1) {
            log("Started in ctx1")
            withContext(ctx2) {
                log("Working in ctx2")
            }
            log("Back to ctx1")
        }
    }
}

Child

코루틴이 다른 코루틴에서 실행되면 해당 코루틴의 context를 그대로 상속받는다. 즉, parent/child 개념으로 만약 parent가 종료되면 child 또한 종료된다는 의미이다.

// launch a coroutine to process some kind of incoming request
val request = launch {
    // it spawns two other jobs, one with GlobalScope
    GlobalScope.launch {
        println("job1: I run in GlobalScope and execute independently!")
        delay(1000)
        println("job1: I am not affected by cancellation of the request")
    }
    // and the other inherits the parent context
    launch {
        delay(100)
        println("job2: I am a child of the request coroutine")
        delay(1000)
        println("job2: I will not execute this line if my parent request is cancelled")
    }
}
delay(500)
request.cancel() // cancel processing of the request
delay(1000) // delay a second to see what happens
println("main: Who has survived request cancellation?")

위의 코드를 살펴보면, request.cancel()이 되면 launch에서 실행된 코루틴들이 모두 종료된다. (반대로 기다릴 수도 있다.)

하지만!! GlobalScope는 예외이다. 이름에서도 알 수 있듯이, GlobalScope는 독립적으로 동작하기 때문에 아무리 코루틴 내부에 코루틴을 만들어도 Parent/Child로 묶이지 않는다.

 

Reference

- kotlinlang.org/docs/reference/coroutines/flow.html#asynchronous-flow