Coroutine Context and Dispatchers

CmplxN·2021년 1월 19일
0

코틀린 공식문서

Dispatchers and threads

  • CoroutineContext에는 Dispatcher정보가 들어있고, Dispatcher정보를 통해 어떤 스레드에서 실행될지 정할 수 있다.
  • Dispatchers에는 Default,IO, Main, Unconfined 등이 있다.
launch {  } // inherits the parent scope's context
launch(Dispatchers.Unconfined) {  } // described below
launch(Dispatchers.Default) {  } // GlobalScope서의 launch와 같은 context, uses a shared background pool of threads.
launch(newSingleThreadContext("MyOwnThread")) {  } // will get its own new dedicated thread (high cost)

Unconfined vs confined dispatcher

  • Unconfined : caller thread에서 시작되지만, suspend에서 돌아오면 call된 suspend function에 의해 thread가 변경될 수 있음.
    특정 thread에서 동작해야되면 Unconfined 사용하지 말것.
launch(Dispatchers.Unconfined) { // not confined -- will work with main thread
    println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
    delay(500)
    // resumes in the default executor thread that the delay function is using
    println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
}
  • coroutine이 즉시 수행되어야하기 때문에 스레드 전환을 곧바로 하지 않았으면 했을 때(?)

Debugging coroutines and threads

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
  • 이제 위 log()를 호출하면 코루틴의 이름까지 나온다.

Jumping between threads

newSingleThreadContext("Ctx1").use { ctx1 -> // use 함수로 block 실행 후 스레드 release
    newSingleThreadContext("Ctx2").use { ctx2 ->
        runBlocking(ctx1) { // set coroutine context to ctx1
            log("Started in ctx1")
            withContext(ctx2) { // change context of coroutine to ctx2
                log("Working in ctx2")
            }
            log("Back to ctx1")
        }
    }
}

Job in the context

  • coroutine의 Job은 그 context의 일부다.
val what = launch {  }
println("My job is ${coroutineContext[Job]}")
println("My job is $what")

// My job is "coroutine#1":BlockingCoroutine{Active}@deb6432
// My job is "coroutine#2":StandaloneCoroutine{Active}@28ba21f3
  • CoroutineScope 안에서의 isActivecoroutineContext[Job]?.isActive == true와 같다.

Children of a coroutine

  • 부모 CoroutineScope에서 새 coroutine이 실행되면 부모의 CoroutineContext를 이어받고, 새 coroutine job은 부모 coroutine job의 child가 된다.
// 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?")
  • 위 예제를 실행하면 job2의 경우 말했던 대로 recursive하게 자식 job도 cancel됨을 확인할 수 있다.
  • job1은 GlobalScope에서 launch했기 때문에 job1은 parent가 없고, cancel되지 않는다.

Parental responsibilities

  • parent coroutine은 기본적으로(join 없이도) children coroutine의 완료를 기다린다.
// 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이 실행됨.
println("Now processing of the request is complete")

Naming coroutines for debugging

  • 디버깅 / 로깅을 위해 coroutine의 이름을 정해줄 수 있다.
async(CoroutineName("v1coroutine")) {  }
  • 위에 언급한 JVM 옵션을 주고 실행하면 지정한 이름을 확인할 수 있다.

Combining context elements

  • CoroutineContext는 아래처럼 +연산으로 필요한 context 요소를 구성할 수 있다.
launch(Dispatchers.Default + CoroutineName("test")) {  }

Coroutine scope

  • 안드로이드 Activity처럼 lifecycle을 가지지만 coroutine이 아닌 객체를 어떻게 다루는지 보자. (cancel)
  • 이럴 때는 CoroutineScope를 직접 만들어 lifecycle 관련 호출 함수에서 cancel()을 거는 식으로 관리할 수 있다.
  • 공식문서에서 제공하는 예제를 참고하자.
class Activity {
    private val mainScope = MainScope()
    
    fun destroy() { // lifecycle destroy
        mainScope.cancel()
    }

    fun doSomething() {
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L)
                println("Coroutine $i is done")
            }
        }
    }
}
  • 아래처럼 처리할 수도 있다.
class Activity : CoroutineScope {
    lateinit var job: Job
    override val coroutineContext: CoroutineContext = job + Dispatchers.Main
   
    // use coroutine scope
}

Thread-local data(?)

  • ThreadLocal을 coroutine에 bound하게 전달할 수 있다. (ThreadLocal.asContextElement())
  • coroutine 내부용으로, 스레드가 바뀌어도 이 데이터는 유지된다.
val threadLocal = ThreadLocal<String?>()
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()}'")
  • 별도 설정이 없으면 child coroutine에서도 같은 값을 get()할 수 있다.
  • 값을 전달할 때는 반드시 withContext() + ThreadLocal.asContextElement()를 사용하자.
  • 그냥 ThreadLocal.set()을 쓰면 예상치 못한 결과가 나올 것이다.
profile
Android Developer

0개의 댓글