코틀린 공식문서
Dispatchers and threads
CoroutineContext
에는 Dispatcher정보가 들어있고, Dispatcher정보를 통해 어떤 스레드에서 실행될지 정할 수 있다.
- Dispatchers에는
Default
,IO
, Main
, Unconfined
등이 있다.
launch { }
launch(Dispatchers.Unconfined) { }
launch(Dispatchers.Default) { }
launch(newSingleThreadContext("MyOwnThread")) { }
Unconfined vs confined dispatcher
- Unconfined : caller thread에서 시작되지만, suspend에서 돌아오면 call된 suspend function에 의해 thread가 변경될 수 있음.
특정 thread에서 동작해야되면 Unconfined 사용하지 말것.
launch(Dispatchers.Unconfined) {
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
delay(500)
println("Unconfined : After delay in thread ${Thread.currentThread().name}")
}
- coroutine이 즉시 수행되어야하기 때문에 스레드 전환을 곧바로 하지 않았으면 했을 때(?)
Debugging coroutines and threads
- IDE를 이용한 디버깅
- Dkotlinx.coroutines.debug를 JVM option에 추가하면 coroutine에 대한 정보도 추가로 확인 가능하다.
- 방법을 잘 모르겠으면 스택오버플로우를 찾아보자.
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
- 이제 위
log()
를 호출하면 코루틴의 이름까지 나온다.
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")
}
}
}
Job in the context
- coroutine의
Job
은 그 context의 일부다.
val what = launch { }
println("My job is ${coroutineContext[Job]}")
println("My job is $what")
- CoroutineScope 안에서의
isActive
는 coroutineContext[Job]?.isActive == true
와 같다.
Children of a coroutine
- 부모 CoroutineScope에서 새 coroutine이 실행되면 부모의 CoroutineContext를 이어받고, 새 coroutine job은 부모 coroutine job의 child가 된다.
val request = launch {
GlobalScope.launch {
println("job1: I run in GlobalScope and execute independently!")
delay(1000)
println("job1: I am not affected by cancellation of the request")
}
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()
delay(1000)
println("main: Who has survived request cancellation?")
- 위 예제를 실행하면 job2의 경우 말했던 대로 recursive하게 자식 job도 cancel됨을 확인할 수 있다.
- job1은 GlobalScope에서 launch했기 때문에 job1은 parent가 없고, cancel되지 않는다.
Parental responsibilities
- parent coroutine은 기본적으로(join 없이도) children coroutine의 완료를 기다린다.
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")
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() {
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
}
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()
을 쓰면 예상치 못한 결과가 나올 것이다.