ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Coroutine] 코루틴 학습 - 5 (Coroutine context)
    Java/Kotlin 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

    반응형

    댓글

Designed by Tistory.