[Android] 코루틴 Coroutine Context and Dispatchers

    이번에는 coroutine context와 dispatchers 에 대해 알아보겠습니다

     

     

    [Dispatchers and threads]

    Coroutine 은 CoroutineContext 에 의해 실행됩니다

    CoroutineContext 의 요소에 여러 요소를 설정할수 있는데 요소들 중에는 Job, Dispatchers 등이 있습니다

    Dispatchers 는 Coroutine이 어떤 thread 나 thread pool 에서 실행될지를 결정하는 요소입니다

     

    모든 Coroutine builder 는 옵셔널로 CoroutineContext 를 파라미터로 받습니다. ex) launch, async

    이것을 통해 dispatcher를 지정할 수 있습니다

      
      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
      Default               : I'm working in thread DefaultDispatcher-worker-1
      newSingleThreadContext: I'm working in thread MyOwnThread
      main runBlocking      : I'm working in thread main
      

    CoroutineContext 없이 사용 된 launch{ .. } 의 경우 부모 CoroutineScope에서 CoroutineContext를 상속 받게 됩니다

    위 예제의 경우 main thread에서 실행되는 main runBlocking 의 CoroutineContext를 상속 받게 됩니다

     

    Unconfined 는 main thread에서 실행된것처럼 보이나 다른 메커니즘 이라고 합니다

     

    Default는 DefaultDispatcher-worker-1 에서 실행되었습니다

    Default는 background thread pool을 공유해서 사용하므로 GlobalScope.launch 와 같습니다

    하지만 cancel 할때에는 둘의 동작이 다릅니다

     

    newSingleThreadContext는 Coroutine를 실행할 새로운 thread를 생성합니다

    singleThreadPool을 만들어 사용하는건 비용이 많이 듭니다.

    그렇기 때문에 사용하지 않을때에는 반드시 close를 해줘야 합니다

    아니면 최상위 변수에 저장한 후 재활용 하는 형태로 사용해야 합니다

     

     

     

    [Debugging coroutines and threads]

      
      fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
      
      fun main() = runBlocking<Unit> {
          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()}")    
      }
      

    Coroutine은 suspend와 resume이 반복됨에 따라 thread가 변경 될수도 있고, 하나의 thread만 사용한다 하더라도 여러개의 코루틴이 어떤 순서로 실행될지 확인하기 어렵습니다.

    kotlinx.coroutines는 Coroutine을 디버깅 하기 위한 옵션을 제공합니다

    -Dkotlinx.coroutines.debug를 JVM option에 추가하면 coroutine을 디버깅 할수 있습니다

      
      [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
      

    runBlocking와 두개의 async가 있으므로 세개의 로그가 찍히게 됩니다

    JVM에서 생성되는 순서대로 숫자가 매겨지게 됩니다

     

     

     

    [Jumping between threads]

    한 Coroutine내에서 thread의 변경을 withContext를 이용하여 용이하게 할 수 있습니다

      
      fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
    
      fun main() {
          newSingleThreadContext("Ctx1").use { ctx1 ->
              newSingleThreadContext("Ctx2").use { ctx2 ->
                  runBlocking(ctx1) {
                      log("Started in ctx1")
                      withContext(ctx2) {
                          log("Working in ctx2")
                      }
                      log("Back to ctx1")
                  }
              }
          }    
      }
      

    실행 결과

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

    위의 예제를 보면 withContext를 이용하여 thread는 변경되지만 coroutine은 하나임을 알 수 있습니다

    withContext를 이용 함으로써 sequential 하게 만들어 줄수 있습니다

     

     

     

    [Job in the context]

      
      fun main() = runBlocking<Unit> {
          println("My job is ${coroutineContext[Job]}")    
      }
      

    실행 결과

      
      My job is "coroutine#1":BlockingCoroutine{Active}@6d311334
      
    

    coroutineContext의 요소인 Job을 coroutineContext[Job] 을 통해서 꺼내올수 있습니다

    isActive는 확장 프로퍼티로 coroutineContext[Job].isActive == true와 같습니다 



     

     

    [Children of a coroutine]

    CoroutineScope 안에 또 다른 Coroutine을 만들게되면 해당 Coroutine은 부모의 CoroutineContext를 상속 받게 됩니다

    이때 부모 coroutine이 cancel 되면 자식 coroutine도 cancel 됩니다

      
      fun main() = runBlocking<Unit> {
          // 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?")
      }
      

    실행 결과

      
      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?
      

    GlobalScope는 어플리케이션 생명주기에 종속되기 때문에 request의 자식 Coroutine이 아닙니다

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

     

     

     

    [Parental responsibilities]

    부모 Coroutine은 자식 Coroutine의 실행이 완료될 때까지 대기합니다

    따라서 join을 사용할 필요가 없습니다

      
      fun main() = runBlocking<Unit> {
          // launch a coroutine to process some kind of incoming request
          val request = launch {
              repeat(3) { i -> // launch a few children jobs
                  launch  {
                      delay((i + 1) * 200L) // variable 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() // wait for completion of the request, including all its children
          println("Now processing of the request is complete")
      }
      

    실행 결과

      
      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
      

     

     

     

    [Combinding context elements]

    CoroutineContext에 여러 요소를 정의할 수 있습니다

    이때 + 연산자를 사용할 수 있습니다

      
      fun main() = runBlocking<Unit> {
          launch(Dispatchers.Default + CoroutineName("test")) {
              println("I'm working in thread ${Thread.currentThread().name}")
          }    
      }
      

    실행 결과

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

     

     

     

    [Coroutine Scope]

    생명주기가 있는 객체가 있지만 해당 객체는 코루틴이 아니라고 한다면 코루틴의 활동이 종료될때 메모리 누수를 방지하기 위해서 코루틴을 취소해주어야 합니다

    Job 객체를 생성해서 처리해 줄 수 있습니다

      
      class Activity {
          private val mainScope = CoroutineScope(Dispatchers.Default) // use Default for test purposes
          
          fun destroy() {
              mainScope.cancel()
          }
    
          fun doSomething() {
              // launch ten coroutines for a demo, each working for a different time
              repeat(10) { i ->
                  mainScope.launch {
                      delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
                      println("Coroutine $i is done")
                  }
              }
          }
      } // class Activity ends
    
      fun main() = runBlocking<Unit> {
          val activity = Activity()
          activity.doSomething() // run test function
          println("Launched coroutines")
          delay(500L) // delay for half a second
          println("Destroying activity!")
          activity.destroy() // cancels all coroutines
          delay(1000) // visually confirm that they don't work    
      }
      

    실행 결과

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

    ※ ktx를 이용한다면 lifecycleScope와 viewmodelScope를 사용하여 취소 작업을 따로 해주지 않아도 알아서 관리합니다

     

    댓글