코틀린/Kotlin Coroutine
[Kotlin][Coroutine] CoroutineContext
sxunea
2024. 7. 2. 22:39
본 글은 조세영님의 <코틀린 코루틴의 정석>을 읽고 이를 바탕으로 작성되었습니다.
코루틴 빌더 lanch, async의 내부 코드
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
앞선 포스트들의 코루틴 빌더 함수들의 내부 코드를 보면 coroutineContext가 반복적으로 등장한다. 이번 포스트에서는 이 coroutineContext가 무엇인지, 또 어떻게 구성하고 제거하고 접근하는지에 대해 알아보도록 하자.
CoroutineContext
CoroutineContext는 코루틴을 실행하는 실행환경을 설정하고 관리하는 인터페이스이다.
CoroutineContext의 구성요소
CoroutineContext의 구성요소는 다음과 같고, 싱글톤 객체로 키 - 값의 구조를 이루고 있다.
- CoroutineName : 코루틴의 이름
- CoroutineDispatcher : 코루틴을 스레드에 할당해 실행
- Job : 코루틴의 추상체로 코루틴을 조작하는데 사용
- CoroutineExceptionHandler : 코루틴에서 발생한 예외 처리
생성하기, 덮어쓰기
- '+' 연산자로 이어서 구성할 수 있다.
- 각 키-값은 고유하므로 덮어쓰는 경우 나중에 들어온 값만을 취한다
fun main() = runBlocking<Unit> {
val coroutineContext: CoroutineContext = newSingleThreadContext("MyThread") + CoroutineName("MyCoroutine")
// 여기서는 coroutineDispatcher, coroutineName 두 개 구성함 나머지는 미설정
launch(context = coroutineContext) {
println("[${Thread.currentThread().name}] 실행")
}
}
합치기
val coroutineContext1 = CoroutineName("MyCoroutine1") + newSingleThreadContext("MyThread1")
val coroutineContext2 = CoroutineName("MyCoroutine2") + newSingleThreadContext("MyThread2")
val combinedCoroutineContext = coroutineContext1 + coroutineContext2
이 경우 coroutineContext1는 coroutineContext2에 의해 덮어씌워진다.
접근하기
- 구성요소의 key 프로퍼티는 동반객체로 선언된 Key와 동일한 객체를 가리킨다
- 아래의 프린트문 세가지가 모두 같은 객체를 가리킨다는 것을 꼭 보고 넘어가자
fun main() = runBlocking<Unit> {
val coroutineName : CoroutineName = CoroutineName("MyCoroutine")
val dispatcher : CoroutineDispatcher = Dispatchers.IO
val coroutineContext = coroutineName + dispatcher
println(coroutineContext[CoroutineName]) // CoroutineName("MyCoroutine")
println(coroutineContext[CoroutineName.Key]) // CoroutineName("MyCoroutine")
println(coroutineContext[coroutineName.key]) // CoroutineName("MyCoroutine")
println(coroutineContext[dispatcher.key]) // Dispatchers.IO
}
제거하기
- minusKey : 구성요소의 키를 인자로 받아 해당 구성 요소를 제거한 coroutineContext 키를 반환한다
- 이때 minusKey를 호출한 coroutineContext 객체는 유지되고, 구성 요소가 제거된 새로운 coroutineContext가 반환된다는 것
@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking<Unit> {
val coroutineName = CoroutineName("MyCoroutine")
val dispatcher = Dispatchers.IO
val job = Job()
val coroutineContext: CoroutineContext = coroutineName + dispatcher + job
val deletedCoroutineContext = coroutineContext.minusKey(CoroutineName)
println("[deletedCoroutineContext] name : " + deletedCoroutineContext[CoroutineName] + ", dispatcher : " + deletedCoroutineContext[CoroutineDispatcher] + ", job : " + deletedCoroutineContext[Job])
println("[coroutineContext] name : " + coroutineContext[CoroutineName] + ", dispatcher : " + coroutineContext[CoroutineDispatcher] + ", job : " + coroutineContext[Job])
}
//[deletedCoroutineContext] name : null, dispatcher : Dispatchers.IO, job : JobImpl{Active}@4b4523f8
//[coroutineContext] name : CoroutineName(MyCoroutine), dispatcher : Dispatchers.IO, job : JobImpl{Active}@4b4523f8
위의 예제를 보면, coroutineContext에서 minusKey를 해주고나면 deletedCoroutineContext는 제거가 완료된 새롭게 반환된 컨텍스트이고, 기존 coroutineContext는 제거 없이 기존 구성 요소를 유지하는 것을 알 수 있다.