ABOUT ME

-

  • Kotlin) Coroutine 공식 가이드 번역 04 - Coroutine Context and Dispatchers
    Kotlin 2021. 1. 15. 21:34


    디스패처와 스레드

    코루틴은 항상 Coroutine Context로 대표되는 어떤 context에서 실행됩니다.

    코루틴의 context중 main 요소는 Job, dispatcher 입니다.

     

    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}")
    }

     

    코루틴 컨텍스트는 어떤 쓰레드에서 해당 코루틴을 실행할지에 대한 dispathcer 정보를 담고 있습니다.

    dispatcher 는 특정 스레드를 지정하거나 , thread pool을 지정하거나, 지정없이 사용가능합니다.

     

    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를 사용한다면 부모 Coroutine Scope의 context와 dispatcher를 그대로 상속받습니다.

     

    Dispatchers.DefaultGlobalScope에서 launch 시킨것과 동일합니다.

     

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


    Unconfined 디스패처 vs Confined 디스패처

    디스패처를 Unconfined로 설정하면, 해당 코루틴은 caller thread에서 시작됩니다.

    단, 이 코루틴이 suspend 되었다가 상태가 재시작 되면 적절한 thread에 재할당되어 시작됩니다.

    따라서 Unconfined는 PU 시간을 소비하지 않고 UI와 같은 특정 스레드에 제한된 공유 데이터를 업데이트 시키지 않을 때 적합합니다.

    (즉, UI처럼 메인 스레드에서 수행되는 작업들은 안됩니다.)

     

    dispatcher의 기본값은 외부 Coroutine Scope의 값을 상속 받습니다.

    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}")
    }

    따라서 내부에서 코루틴이 시작되는 경우 해당 코루틴들은 FIFO로 진행됩니다.

    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

    코루틴과 스레드 디버깅

    코루틴은 suspend와 resume이 반복되면서 다른 thread로 변경될 수 있습니다. 따라서 하나의 thread만 사용하는 dispatcher라고 하더라도 현재 어떤 코루틴이 수행중인지는 확인하기 힘듭니다.

     

    코루틴을 사용할 때, 스레드 이름만으로는 많은 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()}")
    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

    스레드간의 이동

    아래의 예제는 하나의 코루틴이 어떻게 다른 thread에서 분리되어 실행되는지를 보여줍니다.

    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")
            }
        }
    }
    result
    [Ctx1 @coroutine#1] Started in ctx1
    [Ctx2 @coroutine#1] Working in ctx2
    [Ctx1 @coroutine#1] Back to ctx1

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


    Context의 Job

    Job 역시 context의 일부입니다. 따라서 coroutineContext[Job]을 통해 job을 코드안에서도 꺼내 올 수 있습니다.

     

    code
    println("My job is ${coroutineContext[Job]}")
    
    println("My job is Active? : ${coroutineContext[Job]?.isActive == true}")
    
    println("My job is Active? : $isActive")
    result
    My job is "coroutine#1":BlockingCoroutine{Active}@573fd745
    My job is Active? : true
    My job is Active? : true

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


    Coroutine의 자식(하위요소)

    Coroutine Scope에서 다른 coroutine을 launch 시키면 해당 코루틴은 CoroutineScope.coroutineContext를 상속받습니다.

    이때 같이 새로 생성되는 Job 역시 부모 코루틴 Job의 자식이 됩니다.

     

    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?

    부모 코루틴이 취소되면 자식들 역시 recursive하게 전부 취소됩니다.

    위의 예제에서 job1은 GlobalScope에서 띄운 launch이기 때문에 requst의 자식이 아닙니다.

    따라서 request Job을 cancel 하더라도 job1이 취소되지 않습니다.


    상위요소의 책임

    부모 코루틴은 자식 코루틴이 끝날때까지 항상 대기합니다.

    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

    여기서 쓰인 join()은 로그 순서를 보장하기 위해 사용되었습니다. join()을 제거하더라도 모든 로그를 찍고 main() 함수가 종료됩니다.


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

    thread에 이름을 주듯 coroutine에도 이름을 줄 수 있습니다.

     

    code
    fun main() = runBlocking(CoroutineName("main")) {
        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

    Context 요소 결합

    context는 여러 요소를 가질 수 있습니다.

    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

    code
    class Activity {
        private val mainScope = MainScope()
        
        fun destroy() {
            mainScope.cancel()
        }
       // 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
    
    fun main() = runBlocking {
    	val activity = Activity()
    	activity.doSomething() // test 함수 실행
    	println("Launched coroutines")
    	delay(500L) // 0.5초간 delay
    	println("Destroying activity!")
    	activity.destroy() // 모든 코루틴 취소
    	delay(1000) // 코루틴이 동작하지 않음을 시각적으로 확인
    }

    안드로이드의 액티비티는 생명주기를 가지고 있습니다.

    예를 들어, 어플리케이션을 작성하며 Android Activity의 컨텍스트에서 다양한 코루틴을 실행하여 데이터 로드 및 업데이트, 애니메이션 수행 등의 비동기 작업을 수행합니다. 이러한 모든 코루틴은 메모리 누수(memory leak)를 방지하기 위해 Activity가 소멸(destroyed)될 때 취소되어야 합니다.

     

    액티비티의 생명주기와 연관된 CoroutineScope의 인스턴스를 생성하여 코루틴의 생명주기를 관리합니다.

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


    Thread-local 데이터

    Thread를 시작할 때 해당 Thread 내부에서만 사용하기 위한 값으로 local data를 넘겨줘야 하는 경우가 있습니다.

    코루틴은 고정된 쓰레드에서만 수행되지 않고, 지속적으로 다른 쓰레드로 변경되면서 수행될 수 있어 이런 값을 공유하여 사용하기는 어렵습니다.

     

    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()}'")
    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'
    

    위의 예제는 Dispathcer.Default를 사용하여 백그라운드 스레드 풀에서 새로운 코루틴을 시작합니다.

    그래서 스레드 풀과 다른 스레드에서 작동합니다.

    이때 threadLocal.asContextElement로 "launch" 값을 설정했기 때문에 어떤 thread에서 해당 코루틴이 수행되든 ThreadLocal 값은 "launch"를 유지합니다.

    반응형

    댓글

Designed by Me.