ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 공식 가이드 문서로 Coroutine 공부하기 - [04. Coroutine Context and Dispatchers]
    개발/Kotlin 2020. 12. 2. 10:57

    [ 04. 코루틴 컨텍스트 및 디스패처 ]

     


     

    코루틴은 항상 Kotlin 표준 라이브러리에 정의된 CoroutineContext 타입의 값을 대표하는 일부 context 내에서 실행됩니다.

     

    Coroutine context는 다양한 요소의 집합입니다. 주요 요소는 이전에 보았던 코루틴의 Job, 이번 섹션에서 다루는 dispatcher입니다.

     

     

    # 디스패처와 스레드


    Coroutine context에는 해당 코루틴이 실행을 위해 사용하는 스레드를 결정하는 coroutine dispatcher(CoroutineDispatcher 참조)를 참조합니다. coroutine dispatcher는 코루틴 실행을 특정 스레드에 제한하거나 스레드 풀로 디스패치하거나 제한없이 실행되도록 할 수 있습니다.

     

    launchasync 같은 모든 코루틴 빌더는 새로운 코루틴과 기타 context 요소에 대한 특정 디스패처를 명시적으로 지정하는 데 사용할 수 있는 선택적 CoroutineContext 파라미터를 허용합니다.

     

    - code [전체코드 확인하기] : 

    launch { // 상위 컨텍스트, main runBlocking 코루틴
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { 제한되지 않음 -- 메인 스레드와 함께 실행
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // DefaultDispatcher에서 사용
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // 자체 새로운 스레드 사용
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }

     

    - result :

    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

     

    아마 순서가 다른, 위와 같은 출력값을 볼 수 있습니다.

    파라미터 없이 launch { ... } 를 사용하였을 때, 자신이 시작된 CoroutineScope(따라서, 디스패처)에서 context를 상속합니다. 이 경우, main 스레드에서 실행되는 main runBlocking 코루틴의 context를 상속합니다.

     

    Dispatchers.Unconfined 또한 main 스레드에서 실행되는 것처럼 보이는 특수한 디스패처이지만 실제로는 나중에 설명할 다른 매커니즘 입니다.

     

    코루틴이 GlobalScope에서 시작될 때 사용되는 기본 디스패처는 Dispatchers.Default로 표시되며 스레드의 공유 백그라운드 풀을 사용하므로 launch(Dispatchers.Default) { ... }GlobalScope.launch { ... } 동일한 디스패처를 사용합니다.

     

    newSingleThreadContext는 코루틴이 실행할 스레드를 생성합니다. 전용 스레드는 비용이 매우 많이 드는 리소스입니다. 실제 어플리케이션에서는 더 이상 필요하지 않다면, close 함수를 사용하여 해제하거나 최상위 레벨의 변수에 저장하고 어플리케이션 전체에서 재사용해야합니다.

     

     

     

     

    # Unconfined 디스패처  VS  confined 디스패처


    Dispatchers.Unconfined 코루틴 디스패처는 caller thread에서 첫 suspend 지점까지만 코루틴을 시작합니다. suspendsion 후, 호출 된 suspending function에 의해 적절히 결정된 thread에서 코루틴을 다시 시작합니다. Unconfined Dispatcher는 CPU 시간을 소비하지 않고 UI와 같은 특정 스레드에 제한된 공유 데이터를 업데이트 시키지 않을 때 적합합니다.

     

    반면, 디스패처는 기본적으로 외부 CoroutineScope에서 상속됩니다. runBlocking 코루틴에 대한 기본 Dispatcher는 이를 호출한 thread로 제한되어 있으므로, 이를 상속하면 예측 가능한 FIFO 스케줄링을 사용하여 해당 thread로 작업 실행을 제한하는 효과가 있습니다.

     

    - code [전체코드 확인하기] :

    launch(Dispatchers.Unconfined) { // not confined -- main thread에서 작동
        println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
    }
    launch { // 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}")
    }

     

    - result :

    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

     

    runBlocking {...}에서 상속된 context가 있는 코루틴은 재시작하여도 main thread에서 실행을 이어나갑니다. 그러나 unconfined 스레드는 delay 함수가 사용하는 default execution thread에서 재시작합니다.

     


    Unconfined dispatcher는 코루틴의 일부 작업을 즉시 수행해야하기 때문에, 나중에 실행하기 위한 코루틴 Dispatching이 필요 없거나 불필요한 Side-effect를 생성하는 특정 코너 케이스의 경우 유용한 고급 매커니즘 입니다. Unconfined dispatcher는 일반 코드에서 사용해서는 안됩니다.

     

     

     

     

    # 코루틴과 스레드 디버깅


    코루틴은 한 스레드에서 suspend 하고 다른 스레드에서 재시작할 수 있습니다. single-threaded dispatcher를 사용하더라도 특별한 도구가 없다면 코루틴이 무엇을, 어디서, 언제 수행했는지 파악하기 어려울 수 있습니다.

     

     

    # IDEA로 디버깅

    Kotlin 플러그인의 Coroutine Debugger는 IntelliJ IDEA에서 코루틴 디버깅을 단순화 합니다.

     

    kotlinx-coroutines-core 1.3.8 버전 이후에서만 디버깅 가능합니다.

     

    Debug 도구 창에는 Coroutines탭이 있습니다. 해당 탭에서 현재 running 상태의 코루틴과 suspend 상태의 코루틴에 대한 정보를 찾아볼 수 있습니다. 코루틴은 실행 중인 디스패처별로 그룹화 됩니다.

     

    Coroutine debugger로 수행 가능한 것 :

    • 각 코루틴의 상태 체크
    • running 상태 코루틴과 suspend 상태 코루틴에 대한 로컬 value와 캡처된 variable 확인
    • 전체 코루틴 생성 스택과 코루틴 내부의 호출 스택 확인. 스택에는 표준 디버깅중 유실 될 수 있는 프레임이라도 가변값(variable value)이 있는 모든 프레임이 포함됩니다.
    • 각 코루틴에 대한 상태와 스택이 포함된 모든 report를 가져옵니다. 이를 확인하려면 Coroutines 탭에서 오른쪽 마우스 버튼 클릭 후, Get Coroutines Dump를 클릭합니다.

     

    코루틴 디버깅을 시작하려면, break point를 지정하고 debug mode로 어플리케이션을 실행하기만 하면 됩니다.

     

    tutorial에서 코루틴 디버깅에 대해 더 자세히 확인할 수 있습니다.

     

     

    # 로깅을 사용한 디버깅

    스레드를 사용하는 어플리케이션을 Coroutine Debugger 없이 디버깅하는 또 다른 방법은 각 log 명령문의 log 파일에 스레드 이름을 출력하는 것입니다. 해당 기능은 logging framework에서 보편적으로 지원됩니다. 코루틴을 사용할 때, 스레드 이름만으로는 많은 context를 제공하지 않으므로, kotlinx.coroutines에는 보다 편리하게 사용할 수 있는 디버깅 기능이 포함되어 있습니다.

    -Dkotlinx.coroutines.debug JVM 옵션으로 다음 코드를 실행해보십시오.

     

    - code [전체코드 확인하기] :

    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")

     

    세 개의 코루틴이 있습니다. runBlocking 내부의 main coroutine(#1)과 deferred vaue인 a(#2) 및 b(#3)를 계산하는 두 개의 코루틴입니다.

    세 코루틴은 모두 runBlocking 컨텍스트에서 실행되며 main thread에 제한되어 있습니다.

     

    - result :

    [main @coroutine#2] I'm computing a piece of the answer
    [main @coroutine#3] I'm computing another piece of the answer
    [main @coroutine#1] The answer is 42

     

    log 함수는 대괄호 내부에 스레드 이름을 출력하며, 해당 코드 출력값을 통해 현재 실행 중인 coroutine의 식별자가 추가 된 main thread 임을 알 수 있습니다. 해당 식별자는 디버깅 모드가 켜져있을 때 생성된 모든 코루틴에 연속적으로 할당됩니다.

     


    디버깅 모드는 JVM이 -ea 옵션으로 실행될 때도 켜집니다. DEBUG_PROPERTY_NAME 속성 문서에서 디버깅 기능에 대해 자세히 확인할 수 있습니다.

     

     

     

     

    # 스레드간의 이동(Jumping)


    -Dkotlinx.coroutines.debug JVM 옵션으로 다음 코드를 실행해보십시오. (debug 참조)

     

    - code [전체코드 확인하기] :

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

     

    해당 예제는 몇 가지 새로운 기법을 보여줍니다. 하나는 명시적으로 지정된 context의 runBlocking을 사용하는 것이고, 다른 하나는 동일한 코루틴에 머무르는 동안 코루틴의 context를 변경하기 위해 withContext 함수를 사용하는 것입니다.

     

    - result :

    [Ctx1 @coroutine#1] Started in ctx1
    [Ctx2 @coroutine#1] Working in ctx2
    [Ctx1 @coroutine#1] Back to ctx1

     

    해당 예제에서 Kotlin 표준 라이브러리의 use 함수를 사용하여, 더 이상 필요하지 않을 때 newSingleThreadContext로 생성 된 스레드를 릴리즈합니다.

     

     

     

     

    # Context의 Job


    코루틴의 Job은 context의 일부이며, coroutineContext[Job] 를 통해 정보를 확인할 수 있습니다.

     

    - code [전체코드 확인하기] :

    println("My job is ${coroutineContext[Job]}")

     

    - result :

    My job is "coroutine#1":BlockingCoroutine{Active}@573fd745

     

    Debug mode에서는 위와 같이 출력됩니다.

     

     

    - code#1

    println("My job is Active? : ${coroutineContext[Job]?.isActive == true}")

    - code#2

    println("My job is Active? : $isActive")

     

    - result 

    My job is Active? : true

    * code#1과 code#2는 동일한 값을 출력합니다.

     

    CoroutineScope에서의 isActive는 coroutineContext[Job]?.isActive == true 의 편리한 단축키입니다.

     

     

     

     

    # Coroutine의 자식(하위요소)


    코루틴이 다른 코루틴의 CoroutineScope에서 시작될 때, CoroutineScope.coroutineContext를 통해 컨텍스트를 상속하고 새로운 코루틴의 Job은 상위 코루틴들의 Job의 하위요소가 됩니다. 상위 코루틴이 취소되면 모든 하위 코루틴들도 재귀적으로 취소됩니다.

     

    그러나 GlobalScope을 사용하여 코루틴을 시작할 경우, 새로운 코루틴의 Job에 대한 상위 요소는 존재하지 않습니다. 따라서 시작된 scope와 관련이 없으며 독립적으로 작동합니다.

     

    - code [전체코드 확인하기] :

    // 코루틴을 실행하여 일종의 수신 request 처리
    val request = launch {
        // 두 개의 다른 job을 생성하며, 하나는 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")
        }
        // 다른 하나는 상위 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() // request 처리 취소
    delay(1000) // 어떠한 일이 일어나는 지 확인하기 위해 1초간 delay
    println("main: Who has survived request cancellation?")

     

    - result :

    job1: I run in GlobalScope and execute independently!
    job2: I am a child of the request coroutine
    job1: I am not affected by cancellation of the request
    main: Who has survived request cancellation?

     

     

     

     

    # 상위요소의 책임


    상위 코루틴은 항상 모든 하위 코루틴이 완료될 때까지 대기합니다. 상위 코루틴은 시작하는(launch) 모든 자식요소를 명시적으로 추적할 필요 없으며, 마지막에 하위 요소를 기다리기 위해 Job.join을 사용할 필요없습니다.

     

    - code [전체코드 확인하기] :

    // 코루틴을 실행하여 일종의 수신 request 처리
    val request = launch {
        repeat(3) { i -> // 몇 개의 하위 job 생성 및 실행
            launch  {
                delay((i + 1) * 200L) // 가변적으로 delay (200ms, 400ms, 600ms)
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() // 모든 자식 요소를 포함하여 request가 완료될 때까지 대기
    println("Now processing of the request is complete")

     

    - result :

    request: I'm done and I don't explicitly join my children that are still active
    Coroutine 0 is done
    Coroutine 1 is done
    Coroutine 2 is done
    Now processing of the request is complete

     

     

     

     

    # 디버깅을 위한 코루틴 네이밍


    자동으로 할당된 아이디는 코루틴이 로그에 자주 기록될 때 유용하며, 동일한 코루틴의 로그 레코드를 상호연결하기만 하면 됩니다. 그러나 코루틴이 특정 요청 처리나 특정 백그라운드 작업 수행과 연결되어 있는 경우, 디버깅 목적을 위해 명시적으로 이름을 지정하는 것이 좋습니다. CoroutineName 컨텍스트 요소는 스레드 이름과 동일한 용도로 사용됩니다. debugging mode가 켜져 있는 경우 해당 코루틴을 실행하는 스레드 이름에 CoroutineName이 포함됩니다.

     

    - code [전체코드 확인하기] :

    log("Started main coroutine")
    // 두 개의 background value 계산 실행
    val v1 = async(CoroutineName("v1coroutine")) {
        delay(500)
        log("Computing v1")
        252
    }
    val v2 = async(CoroutineName("v2coroutine")) {
        delay(1000)
        log("Computing v2")
        6
    }
    log("The answer for v1 / v2 = ${v1.await() / v2.await()}")

     

    - result :

    [main @main#1] Started main coroutine
    [main @v1coroutine#2] Computing v1
    [main @v2coroutine#3] Computing v2
    [main @main#1] The answer for v1 / v2 = 42

     

    -Dkotlinx.coroutines.debug JVM 옵션으로 생성되는 출력값은 위와 유사합니다.

     

     

     

     

    # Context 요소 결합


    때때로 Coroutine Context에 대해 여러 요소를 정의해야 할 수도 있습니다. 이러한 경우 + 연산자를 사용할 수 있습니다. 예를 들어, 동시에 특정 dispatcher와 name(Coroutine Name)을 명시적으로 지정한 코루틴을 시작할 수 있습니다.

     

    - code [전체코드 확인하기] :

    launch(Dispatchers.Default + CoroutineName("test")) {
        println("I'm working in thread ${Thread.currentThread().name}")
    }

     

    - result :

    I'm working in thread DefaultDispatcher-worker-1 @test#2

     

     

     

     

    # Coroutine Scope


    context, 하위 요소, job에 대한 지식을 종합해보겠습니다. 어플리케이션이 생명주기(lifecycle)가 있는 객체를 가지고 있으나 해당 객체는 코루틴이 아니라고 가정합니다. 예를 들어, Android 어플리케이션을 작성하며 Android Activity의 컨텍스트에서 다양한 코루틴을 실행하여 데이터 로드 및 업데이트, 애니메이션 수행 등의 비동기 작업을 수행합니다. 이러한 모든 코루틴은 메모리 누수(memory leak)를 방지하기 위해 Activity가 소멸(destroyed)될 때 취소되어야 합니다. 물론 context와 job을 수동으로 조작하여 activity의 생명주기와 코루틴을 결합시킬 수 있으나, kotlinx.coroutinesCoroutineScope를 캡슐화하는 추상화를 제공합니다. 모든 코루틴 빌더는 extension으로 선언되어 있으므로, coroutine scope에 미리 익숙해져야합니다.

     

    우리는 Activity의 생명주기와 연관된 CoroutineScope의 인스턴스를 생성하여 코루틴의 생명주기를 관리합니다. CoroutineScope 인스턴스는 CoroutineScope() 또는 MainScope() factory function으로 생성할 수 있습니다. 전자는 범용 scope를 생성하며 후자는 UI 어플리케이션에 대한 scope를 생성하고 Dispatchers.Main을 기본 디스패처로 사용합니다.

     

    - code [전체코드 확인하기] :

    class Activity {
        private val mainScope = MainScope()
        
        fun destroy() {
            mainScope.cancel()
        }
        // to be continued ...

     

    이제 정의된 scope를 사용하여 본 Activity의 scope에서 코루틴을 시작할 수 있습니다. 예제코드의 경우, 각자 다른 시간을 delay하는 열 개의 코루틴을 실행시킵니다.

     

    - code [전체코드 확인하기] :

    // class Activity continues
        fun doSomething() {
            // 각자 다른 시간동안 작동하는 열 개의 데모용 코루틴 생성 및 실행
            repeat(10) { i ->
                mainScope.launch {
                    delay((i + 1) * 200L) // 가변적으로 delay (200ms, 400ms, ...)
                    println("Coroutine $i is done")
                }
            }
        }
    } // class Activity ends

     

    main 함수에서 activity를 생성하고 테스트용 doSomthing 함수를 호출하며 500ms 후에 activity를 destroy시킵니다. 이는 doSomthing 함수에서 시작된 모든 코루틴을 취소(cancel)합니다. activity가 중단(destruction)된 이후에는 조금 더 대기하더라도 더 이상 메시지가 출력되지 않기 때문에  하위 코루틴이 취소되었음을 확인할 수 있습니다.

     

    - code [전체코드 확인하기] :

    val activity = Activity()
    activity.doSomething() // test 함수 실행
    println("Launched coroutines")
    delay(500L) // 0.5초간 delay
    println("Destroying activity!")
    activity.destroy() // 모든 코루틴 취소
    delay(1000) // 코루틴이 동작하지 않음을 시각적으로 확인

     

    - result :

    Launched coroutines
    Coroutine 0 is done
    Coroutine 1 is done
    Destroying activity!

     

    보시다시피, 처음 두 개의 코루틴만 메시지를 출력하며, 나머지는 Activity.destroy()에서의 job.cancel() 호출만으로 취소됩니다.

     


    Android는 생명주기(lifecycle)가 있는 모든 항목의 coroutine scope에 대한 자사(first-party) 지원을 제공합니다. The corresponding documentation를 확인하십시오.

     

     

     

     

    # Thread-local 데이터


    경우에 따라 일부 thread-local 데이터를 코루틴으로 전달하거나, 코루틴간에 전달 할 수 있는 기능이 있다면 편리합니다. 그러나 코루틴은 어느  특정 스레드에 한정되어 있지 않으므로 직접 구현할 경우 보일러플레이트가 발생할 수 있습니다.

     

    ThreadLocal의 경우, asContextElement extension 함수로 이를 해결할 수 있습니다. 해당 함수는 주어진 ThreadLocal 값을 저장하고 코루틴이 컨텍스트를 전환할 때마다 복원하는 부가적인 context 요소를 생성합니다.

     

    - code [전체코드 확인하기] :

    threadLocal.set("main")
    println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
        println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
        yield()
        println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    }
    job.join()
    println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")

     

    해당 예제 코드는 Dispatchers.Default를 사용하여 백그라운드 스레드 풀에서 새로운 코루틴을 시작하므로 스레드 풀과 다른 스레드에서 작동합니다. 그러나 코루틴이 실행되는 스레드와 관계없이, threadLocal.asContextElement(value = "launch")로 지정한 thread local variable 값을 여전히 가지고 있습니다. 따라서 출력값은(debug 모드) 다음과 같습니다.

     

    - result :

    Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
    Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch'
    After yield, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch'
    Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'

     

     

    적절한 context 요소 설정하는 것을 잊기 쉽습니다. 코루틴을 실행하는 스레드가 다른 경우, 코루틴에서 접근하는 thread-local 변수에 예기치 않은 값을 가지고 있을 수도 있습니다. 이러한 상황을 방지하기위해 ensurePresent 메서드와 fail-fast를 사용하여 부적절한 사용에 대처하는 것이 좋습니다.

     

    ThreadLocal는 first-class를 지원하며 kotlinx.coroutines에서 제공하는 어느 원시(primitive) 타입과 함께 사용할 수 있습니다. 그러나 한 가지 주요 제약 사항 이 있습니다. thread-local이 변경될 경우, 새로운 값이 코루틴 호출자에 전파되지 않고(context 요소가 모든 ThreadLocal 객체 액세스를 추적할 수 없기 때문입니다) 업데이트 된 값이 다음 중단점(suspension)에서 유실된다는 것 입니다.  코루틴에서 thread-local 값을 변경하려면 withContext를 사용하여야 하며, 더 자세한 내용은 asContextElement에서 확인하십시오.

     

    또 다른 방안으로, 값을 class Counter(var i: Int)와 같은 mutable box에 저장할 수 있으며 이는 차례로 thread-local 변수에 저장됩니다. 그러나 이러한 경우, 이 mutable box 변수에 동시에 접근하여 수정하는 상황에 대한 동기화 처리를 개발자가 전적으로 책임져야 합니다.

     

    logging MDC 와의 통합, transactional context 또는 내부적으로 데이터 전달을 위한 thread-local 사용하는 외부 라이브러리와 같은 고급 용도로 사용하는 경우 구현되어야 할 ThreadContextElement 인터페이스의 문서를 참고하십시오.

     

     

     


     

     

    Coroutine Context and Dispatchers - Kotlin Programming Language

     

    kotlinlang.org

     

     

    댓글

Designed by Tistory.