-
기존에 있었던 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 되어 출력된다.
-
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg") 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
class Activity{ private val mainScope = MainScope() ... fun destory(){ mainScope.cancel() } ... }
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 를 참조해보자.
'프로그래밍 놀이터 > Kotlin, Coroutine' 카테고리의 다른 글
[coroutine] exception 을 이해해보자 #1 ( in my coroutine scope ) (0) | 2019.03.05 |
---|---|
[coroutine] Exception handling (0) | 2019.03.04 |
[android] AsyncTask 를 Coroutine 으로 바꿔본 후기 (0) | 2019.02.07 |
[도서 정리] Android Development with Kotlin - Delegates (0) | 2018.12.17 |
[도서 정리] Android Development with Kotlin - Extension Functions and Properties (0) | 2018.12.16 |
댓글