Java/Kotlin

[Coroutine] 코루틴 학습 - 5 (Coroutine context)

Icarus8050 2022. 4. 28. 21:06
반응형

Coroutine context

  • coroutine builder들의 정의를 살펴보면 첫 번째 파라미터에 CoroutineContext 타입을 넘겨받고 있는 것을 확인할 수 있다.
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
  • launch()의 리시버와 마지막 파라미터의 리시버는 CoroutineScope이다.
  • CoroutineScope는 CoroutineContext를 래핑하고 있는 인터페이스이다.
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}
  • 코루틴을 재개하는 Continuation의 정의를 살펴보면 마찬가지로 CoroutineContext를 래핑하고 있는 인터페이스라는 것을 확인할 수 있다.
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}
  • CoroutineContext은 element 또는 element들의 컬렉션을 나타내는 인터페이스이다. (Map, Set 컬렉션과 유사한 컨셉)
    • CoroutineContext는 Job, CoroutineName, CoroutineDispatcher 와 같은 element들의 집합을 인덱싱한다.
  • 모든 element는 유니크한 key를 가지고, CoroutineContext에서 Element를 식별할 때 Key의 참조를 비교한다.
  • 예시로 CoroutineName, Job 등의 클래스들이 Coroutine 인터페이스를 구현하고 있는 CoroutineContext.Element를 구현하고 있다.
fun main() {
    val name: CoroutineName = CoroutineName("A name")
    val element: CoroutineContext.Element = name
    val context: CoroutineContext = element

    val job: Job = Job()
    val jobElement: CoroutineContext.Element = job
    val jobContext: CoroutineContext = jobElement
}

 

CoroutineContext에서 Element 찾기

  • CoroutineContext는 Map과 같은 컬렉션과 비슷한 컨셉이어서 get()를 통해서 CoroutineContext안에 있는 키를 통해서 Element를 찾을 수 있다.
  • CoroutineContext에서 Key를 가지고 탐색했을 때, Element가 존재하면 해당 Element를 리턴하고, 없으면 null을 리턴한다.
fun main() {
    val ctx: CoroutineContext = CoroutineName("Hello World!")

    ctx[CoroutineName]?.let {
        println(it.name)
    } ?: println("CoroutineName is not exists..")

    ctx[Job]?.let {
        println("Job is exists!")
    } ?: println("Job is not exists..")
}
// Hello World!
// Job is not exists..
  • 위 예시에서 CoroutineName을 찾을 때, Key로 찾지 않고 CoroutineName 클래스로 Element를 탐색하는걸 확인할 수 있다.
    • 이는 코틀린의 기능으로, 클래스 이름이 Named이건 아니건, companion object 클래스의 참조로 이용될 수 있다. [참조]
public data class CoroutineName(
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {

    public companion object Key : CoroutineContext.Key<CoroutineName>

    override fun toString(): String = "CoroutineName($name)"
}

 

Adding contexts

fun main() {
    val ctx1: CoroutineContext = CoroutineName("Name1")
    println(ctx1[CoroutineName]?.name)  // Name1
    println(ctx1[Job]?.isActive)   // null

    println()

    val ctx2: CoroutineContext = Job()
    println(ctx2[CoroutineName]?.name)  // null
    println(ctx2[Job]?.isActive)    // true

    println()

    val ctx3 = ctx1 + ctx2
    println(ctx3[CoroutineName]?.name)  // Name1
    println(ctx3[Job]?.isActive)    // true

    println()

    val ctx4: CoroutineContext = CoroutineName("Name2")
    val ctx5 = ctx3 + ctx4
    println(ctx5[CoroutineName]?.name)  // Name2
    println(ctx5[Job]?.isActive)    // true

    println()

    println(ctx3[CoroutineName]?.name)  // Name1
    println(ctx3[Job]?.isActive)    // true
}
  • CoroutineContext는 Key가 다른 CoroutineContext와 쉽게 합칠 수 있다. 아래 예시와 같이 plus operator를 구현하고 있기 때문에 손쉽게 두 컨텍스트를 합칠 수 있다.
  • 만약 이미 존재하는 키를 합치게 되면 기존의 element를 새로운 element로 대체한다.
  •  위의 예시에서 중복된 CoroutineName을 add 한 ctx5는 새로운 element를 대체하여 Name2를 출력하고 있고, add하기 전인 ctx3은 여전히 Name1을 출력하고 있다.

 

Subtracting elements

  • Element는 Key와 minusKey() 함수를 이용하여 context로부터 제거할 수도 있다.
fun main() {
    val ctx = CoroutineName("Name1") + Job()
    println(ctx[CoroutineName]?.name)   // Name1
    println(ctx[Job]?.isActive) // true

    val ctx2 = ctx.minusKey(CoroutineName)
    println(ctx[CoroutineName]?.name)   // null
    println(ctx[Job]?.isActive) // true
}

 

Folding context

  • 컨텍스트 내에 있는 element들에 특정한 작업이 필요하다면 fold() 메서드를 이용하면 된다. 이는 collection의 fold() 함수와 유사하다.
fun main() {
    val ctx = CoroutineName("Name1") + Job()

    ctx.fold("") { acc, element -> "$acc$element" }
        .also(::println)
    // CoroutineName(Name1)JobImpl{Active}@57e1b0c

    val empty = emptyList<CoroutineContext>()
    ctx.fold(empty) { acc, element -> acc + element }
        .joinToString()
        .also(::println)
    // CoroutineName(Name1), JobImpl{Active}@57e1b0c
}

 

Coroutine context and builders

  • CoroutineContext는 자신의 컨텍스트를 자식에게 전파한다. 즉, 자식 컨텍스트는 부모로부터 컨텍스트를 상속받는다.
fun CoroutineScope.log(msg: String) {
    val name = coroutineContext[CoroutineName]?.name
    println("[$name] $msg")
}

fun main() = runBlocking(CoroutineName("main")) {
    log("Started")
    val v1 = async {
        delay(1000)
        log("Running async")
        42
    }
    launch {
        delay(3000)
        log("Running launch")
    }
    log("The answer is ${v1.await()}")
    // [main] The answer is 42
}
// [main] Started
// [main] Running async
// [main] The answer is 42
// [main] Running launch
  • 자식 컨텍스트는 컨텍스트 선언 시, argument를 전달 받아서 부모의 컨텍스트를 덮어쓸 수 있다.
private fun CoroutineScope.log(msg: String) {
    val name = coroutineContext[CoroutineName]?.name
    println("[$name] $msg")
}

fun main() = runBlocking(CoroutineName("main")) {
    log("Started")
    val v1 = async(CoroutineName("C1")) {
        delay(1000)
        log("Running async")    // [C1] Running async
        42
    }
    launch(CoroutineName("C2")) {
        delay(3000)
        log("Running launch")   // [C2] Running launch
    }
    log("The answer is ${v1.await()}")
    // [main] The answer is 42
}
// [main] Started
// [C1] Running async
// [main] The answer is 42
// [C2] Running launch

 

Accessing

suspend fun printName() {
    println(coroutineContext[CoroutineName]?.name)
}

suspend fun main() = withContext(CoroutineName("Outer")) {
    printName() // Outer
    launch(CoroutineName("Innter")) {
        printName() // Inner
    }
    delay(2000)
    printName() // Outer
}
  • CoroutineScope는 coroutineContext를 프로퍼티를 가지고 있다.
  • 해당 컨텍스트는 continuations에 의해 참조되고 있고, Continuation은 일시중단 함수마다 마지막 파라미터로 넘겨진다.
  • 따라서 일시중단 함수 내에서 부모 컨텍스트에 접근할 수 있다.

 


참고자료

https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837

반응형