본문 바로가기
프로그래밍 놀이터/Kotlin

[Coroutine] Coroutine context and dispatchers

by 돼지왕왕돼지 2019. 2. 8.

[Coroutine] Coroutine context and dispatchers


https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/coroutine-context-and-dispatchers.md#thread-local-data


ascontextelement, Async, caller thread, cancel, cancellation vis explicit job, children cancel, children of a coroutine, close function, combining context elements, confined dispatcher, context element, coroutine builder, coroutine context, coroutine context inherit, coroutine debug, Coroutine dispatcher, coroutine name, coroutinecontext override, coroutinecontext param, coroutinecontext type, CoroutineName, coroutinescope, debugging coroutine and threads, debugging mode, default dispatchers, DefaultExecutor, DIspatcher, dispatchers and threads, dispatchers.default, dispatchers.unconfined, element set, globalscope, globalscope coroutine launch, globalscope dispatcher, Job, job in context, Join, jvm option, launch, lifecycle, logging framework, naming coroutine for debugging, newSingleThreadContext, parent coroutine, parent job child, parental responsibilities, Scope, shared bg thread pool, suspending function 수행 thread, suspension, Suspension point, suspension resume, thread local set, thread name log, thread-local data, thread.currentthread, ThreadLocal, threadlocal first-class support, unconfined dispatcher, unconfined dispatchers, what is coroutine context, withContext, [Coroutine] Coroutine context and dispatchers


-

기존에 있었던 doc 이 update 되어서 따로 정리한다.



Coroutine context and dispatchers


-

Coroutine 은 항상 어떤 context 하에서 실행된다.

이 context 는 CoroutineContext type 을 가지며 Kotlin standard lib 에 정의되어 있다.



-

Coroutine context 는 여러개의 element 들을 가진 set 이다.

main element 는 coroutine 의 Job, dispatcher 이다.





Dispatchers and threads


-

Coroutine context 는 coroutine dispatcher 를 포함하며, 이는 어떤 thread 에서 coroutine 을 실행할지를 결정한다.

Coroutine dispatcher 는 coroutine 의 실행을 특정 하나의 thread 에 한정 시킬 수 있고, thread pool 에 던질 수도 있고, 정의되지 않은채로 실행시킬 수도 있다.



-

모든 launch, async 와 같은 모든 coroutine builder 는 CoroutineContext param 을 optional 로 받는다.



-

runBlocking{
    launch{ print(“launch : ${Thread.currentThread().name}”) }
    launch(Dispatchers.Unconfined){ print(“not confined : ${Thread.currentThread().name}”) }
    launch(Dispatchers.Default){ print(“default : ${Thread.currentThread().name}”) }
    launch(newSingleThreadContext(“MyOwnThread”){ print(“singleThread :  ${Thread.currentThread().name}”) }
}

결과의 순서와 DefaultDispatcher-worker-1 의 번호는 다를 수 있다.

Unconfined : main

default : DefaultDispatcher-worker-1

newSingleThread : MyOwnThread

launch : main



-

param 없는 launch 의 경우 실행되는 CoroutineScope 의 context( 그 안의 dispatcher 도 당연히 ) 를 inherit 한다.

위의 예제에서는 runBlocking 이 main 에서 불렸기 때문에 Unconfined 와 launch 가 main 에서 실행된 것이다.



-

Dispatchers.Unconfined 는 특별한 dispatcher 로 inherit 와 비슷하지만 다르다.

이는 나중에 다시 설명한다.



-

Dispatchers.Default 는 GlobalScope 의 기본 dispatcher 이다.

그리고 shared bg thread pool 을 사용한다.

launch(Dispatchers.Default){ ... } 와 GlobalScope.launch{ … } 는 동일한 Default dispatcher 를 사용한다.



-

newSingleThreadContext 는 새로운 thread 를 생성해서 그곳에서 수행한다.

이는 새로운 thread 를 만들기 때문에 res 측면에서 가장 비싸다.

실제 앱에서는 더 이상 사용하지 않는다면 close function 으로 release 시켜주어야 한다. 그렇지 않으면 memory leak 이 발생할 수 있다.

또는 top-level 로 정의해서 app 생명주기 전반에서 재사용해야 한다.





Unconfined vs confined dispatcher


-

Dispatchers.Unconfined 는 caller thread 에서 coroutine 을 수행한다.

그러나 이는 suspension point 까지만 valid 하다.

suspension 이 resume 될 때는 suspending function 을 수행된 thread 에서 resume 된다.

Unconfined dispatcher 는 coroutine 이 CPU time 을 사용하지 않거나, UI 를 비롯하여 특정 thread 에 제한된 shared data 를 update 하지 않는 경우에 사용하는 것이 적절하다.



-

runBlocking{
    launch(Dispatchers.Unconfined){
        print(“Unconfined : I’m working in thread ${Thread.currentThread().name}”)
        delay(500L)
        print(“Unconfined : After delay in thread ${Thread.currentThread().name}”)
    }

    launch{
        print(“Main : I’m working in thread ${Thread.currentThread().name}”)
        delay(1000L)
        print(“Main : Aftrer delay  in thread ${Thread.currentThread().name}”)
    }
}


output 은..


Unconfined : I’m working in thread main

Main : I’m working in thread main

Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor

Main : After delay in thread main



-

Unconfined 의 경우 delay 가 DefaultExecutor 에 의해 호출되기 때문에 resume 은 DefaultExecutor 에서 된다.



-

Unconfined dispatcher 는 고급 메카니즘으로 특정 케이스에 유효하다.

그래서 일반적인 코드에서는 쓰이지 않는 것이 좋다.





Debugging coroutine and threads


-

Coroutine 은 한 thread 에서 실행되어 다른 thread 에서 resume 될 수 있다.

심지어 single-threaded dispatcher 의 경우에도 어떤 일을 어디서 언제 수행하는지를 알아내기 힘들 수 있다.

일반적인 debugging 방법은 thread name 을 log 로 출력하는 것이다.

이는 logging framework 를 통해 쉽게 할 수 있다.

coroutine 을 사용할 때 thread 만 찍으면 context 정보를 충분히 알 수 없기 때문에, kotlinx.coroutines 에 debugging 용도로 함수들이 들어가 있다.



-

'-Dkotlinx.coroutines.debug' JVM option 과 함께 아래 코드를 실행하면..


Thread.currentThread().name 만 출력해도 “main @coroutine#1” 과 같이 출력된다.

JVM 이 -ea 옵션으로 실행되는 한 debug mode 는 항상 켜져 있으며, debug mode 일 때는 coroutine#1 의 숫자는 순차적으로 assign 되어 출력된다.



-

runBlocking{
    val a = async{
        log(“I’m a”)
        6
    }
    val b = async{
        log(“I’m b”)
        7
    }
    log(“answer is ${a.await() * b.await()}”)
}


위의 경우 아래와 같이 3개의 coroutine 이 있다.

answer 를 print 하는 main coroutine (#1)

I’m a 를 print 하는 main coroutine (#2)

I’m b 를 print 하는 main coroutine (#3)






Jumping between threads


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 블록은 같은 coroutine 안에 머물러 있다는 것을 눈여겨보자.

newSingleThreadContext 로 만든 것은 close 를 불러주어야 하는데, kotlin 의 use 를 사용하면 이를 자동으로 할 수 있다.





Job in the context


-

coroutine 의 Job 도 context 의 일부이다.

job 은 coroutineContext[Job] 을 통해 얻어올 수 있다.

( 돼왕 : coroutineContext 는 coroutineScope 에서 바로 접근 가능하다. )



-

debug mode 에서 Job 을 toString 하면 아래와 같은 form 으로 나온다.

“coroutine#1”:BlockingCoroutine{Active}@6d311334 



-

coroutineScope 에서의 isActive 는 사실 coroutineContext[Job]?.isActive == true 의 shortcut 이다.





Children of a coroutine


-

coroutine 이 다른 coroutine 의 CoroutineScope 에서 실행될 때, CoroutineScope.coroutineContext 를 inherit 하게 된다.

그리고 새로운 coroutine 의 Job 이 parent coroutine job 의 child 가 된다.

parent coroutine 이 취소 되면, children 도 모두 cancel 된다.



-

GlobalScope 에서 coroutine 이 launch 되면, launch 된 job 은 GlobalScope 에 Job 이 없어 귀속되지 않고, 독립적으로 실행되게 된다.



-

runBlocking{
    val request = launch{
        GlobalScope.launch{
            println(“I run in GlobalScope and execute independently!”)
            delay(1000L)
            println(“I’m not affected by cancellation of the request”)
        }

        launch{
            delay(100L)
            println(“I’m a child of the request coroutine”)
            delay(1000L)
            println(“I’ll not execute this line if my parent request is cancelled”)
        }
    }
    delay(500L)
    request.cancel()
    delay(1000L)
    println(“Who has survived request cancellation”)
}

결과는..


I run in GlobalScope and execute independently!

I’m a child of the request coroutine

I’m not affected by cancellation of the request

Who has survived request cancellation





Parental responsibilities


-

parent coroutine 은 항상 children 의 완료를 기다린다.

parent 는 명시적으로 children 의 launch 를 track 할 필요가 없고, Job.join 으로 그들을 기다릴 필요도 없다.



-

runBlocking{
    val request = launch{
        repeat(3) { i ->
            launch{
                delay((i + 1) * 200L)
                println(“Coroutine $i is done”)
            }
        }
        println(“request: I’m done and I don’t explicitly join my children that are still active”)
    }
    request.join()
    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 id done

Coroutine 1 id done

Coroutine 2 id done

Now processing of the request is complete





Naming coroutine for debugging


-

자동으로 assign 된 id 도 로그를 자주 찍으며 상호관계를 보기에는 좋다.

그러나 coroutine 이 특정 request 의 processing 에 묶여있거나, 특정 bg task 를 수행한다면, 디버그 용도로 이름을 주는 것이 좋다.

CoroutineName 도 context element 로 thread name 과 같은 기능을 한다.

이 녀석은 debugging mode 가 켜져 있다면, thread name 에서 함께 표시된다.



-

runBlocking(CoroutineName(“main”)){
    log(“Started main coroutine”)
    val v1 = async(CoroutineName(“v1coroutine”)){
        delay(500L)
        log(“Computing v1”)
        252
    }

    val v2 = async(CoroutineName(“v2coroutine”)){
        delay(1000L)
        log(“Computing v2”)
        6
    }

    log(“The answer for v1 / v2 = ${v1.await() / v2.await(){“)
}

결과는..


[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






Combining context elements


-

종종 coroutine context 여러 개의 element 를 정의할 필요가 있다.

이 때 + operator 를 사용한다.

예를 들어 명시적으로 특정 dispatcher 를 사용하며 명시적으로 이름을 지정한 coroutine 을 launch 하려면...


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

결과는..

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





CoroutineScope


-
우리의 context, children, jobs 에 대한 지식을 한군데 모아보자.
우리 앱이 lifecycle 을 가진 object 가 있고, 그것은 coroutine 이 아니라고 가정해보자.
예를 들어 우리가 안드로이드 앱을 만드는데, android activity context 에서 date 의 fetch 와 update, animation 등을 위해 여러개의 coroutine 을 launch 시킨다고 해보자.
이 coroutine 들은 activity 가 destory 될 때 memory leak 을 피하기 위해서 반드시 취소되어야 한다.
우리는 context 와 job 을 수동으로 다루며, activity lifecycle 에 따라 control 을 할 수 있다.
하지만 kotlinx.coroutines 에서는 CoroutineScope 을 encapsulate 하는 추상화를 제공한다.


-
CoroutineScope instance 를 만들고, activity lifecycle 에 묶이도록 할 수 있다.
CoroutineScope instance 는 CoroutineScope() 나 MainScope() factory function 에 의해 생성될 수 있다.
CoroutineScope() 는 일반적 목적의 scope 를 만들고, MainScope() 는 Dispatchers.Main 을 dispathcer 로 사용하는 UI app 을 위한 scope 을 만든다.
class Activity{
    private val mainScope = MainScope()

    ...

    fun destory(){
        mainScope.cancel()
    }

    ...
}


-
-
Activity class 에서 CoroutineScope interface 를 구현할 수도 있다.
가장 추천되는 방법은 기본 factory function 과 함께 delegation 을 사용하는 방법이다.
우리가 원하는 dispatcher 도 지정할 수 있다. (예제에서는 Dispatchers.Default 를 사용한다.)

또한 Activity 안에서 명시적으로 context 를 지정해주지 않고도 coroutine 을 launch 할 수 있다.
class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default){
    ...

    fun doSomething(){
        repeat(10) { i ->
            launch{
                delay((i + 1) * 200L)
                println("Coroutine $i is done")
            }
        }
    }

    fun destroy() {
        cancel()
    }

    ...
}




Cancellation via explicit job - deprecated

위의 "CoroutineScope" 로 대체되었다.


이전 자료를 보고 싶다면 "더보기" 클릭





Thread-local data


-

종종 thread-local data 를 넘길 수 있는 것이 편리하다.

그러나 coroutine 은 특정 thread 에 종속되지 않기 때문에 많은 boilerplate 코드를 통하지 않고 이를 성취하는 것이 어려울 수 있다.



-

ThreadLocal 의 asContextElement extension function 이 구세주가 될 수 있다.

이것은 추가적인 context element 를 만들어서 ThreadLocal 를 담고 있을 수 있으며, coroutine context 가 switch 될 때마다 restore 할 수 있다.



-

val threadLocal = ThreadLocal<String?>()

runBlocking{
    threadLocal.set(“main”)
    println(“Pre-main, current thread: ${Thread.currentThread(), thead 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()}’”)
}


결과는..

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-2 @coroutine#2,5,main], thread local value: 'launch'

Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: ‘main'



-

ThreadLocal 은 first-class support 이며, 1개의 key 만 사용한다는 limit 이 있다.

thread-local 이 변형되었을 때 새로운 value 는 coroutine caller 에게 전파되지 않는다.

그리고 다음 suspension 때 update 된 value 는 다음 suspending point 에 사라진다.

thread-local 값을 변경하려면 withContext 를 사용하는 것이 좋다.



-

대안으로 값이 Counter(var i:Int) 와 같은 mutable box 에 저장될 수 있다.

그리고 이 값이 thread-local variable 로 저장될 수 있다.

하지만 이 경우 synchronize 에 대한 책임이 추가된다.



-

고급 사용을 위해서는 ThreadContextElement 를 참조해보자.




댓글0