-
[Coroutine] 코루틴 학습 - 5 (Coroutine context)Java/Kotlin 2022. 4. 28. 21:06반응형
Coroutine context
- coroutine builder들의 정의를 살펴보면 첫 번째 파라미터에 CoroutineContext 타입을 넘겨받고 있는 것을 확인할 수 있다.
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job
- launch()의 리시버와 마지막 파라미터의 리시버는 CoroutineScope이다.
- CoroutineScope는 CoroutineContext를 래핑하고 있는 인터페이스이다.
public interface CoroutineScope { public val coroutineContext: CoroutineContext }
- 코루틴을 재개하는 Continuation의 정의를 살펴보면 마찬가지로 CoroutineContext를 래핑하고 있는 인터페이스라는 것을 확인할 수 있다.
public interface Continuation<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>) }
- CoroutineContext은 element 또는 element들의 컬렉션을 나타내는 인터페이스이다. (Map, Set 컬렉션과 유사한 컨셉)
- CoroutineContext는 Job, CoroutineName, CoroutineDispatcher 와 같은 element들의 집합을 인덱싱한다.
- 모든 element는 유니크한 key를 가지고, CoroutineContext에서 Element를 식별할 때 Key의 참조를 비교한다.
- 예시로 CoroutineName, Job 등의 클래스들이 Coroutine 인터페이스를 구현하고 있는 CoroutineContext.Element를 구현하고 있다.
fun main() { val name: CoroutineName = CoroutineName("A name") val element: CoroutineContext.Element = name val context: CoroutineContext = element val job: Job = Job() val jobElement: CoroutineContext.Element = job val jobContext: CoroutineContext = jobElement }
CoroutineContext에서 Element 찾기
- CoroutineContext는 Map과 같은 컬렉션과 비슷한 컨셉이어서 get()를 통해서 CoroutineContext안에 있는 키를 통해서 Element를 찾을 수 있다.
- CoroutineContext에서 Key를 가지고 탐색했을 때, Element가 존재하면 해당 Element를 리턴하고, 없으면 null을 리턴한다.
fun main() { val ctx: CoroutineContext = CoroutineName("Hello World!") ctx[CoroutineName]?.let { println(it.name) } ?: println("CoroutineName is not exists..") ctx[Job]?.let { println("Job is exists!") } ?: println("Job is not exists..") } // Hello World! // Job is not exists..
- 위 예시에서 CoroutineName을 찾을 때, Key로 찾지 않고 CoroutineName 클래스로 Element를 탐색하는걸 확인할 수 있다.
- 이는 코틀린의 기능으로, 클래스 이름이 Named이건 아니건, companion object 클래스의 참조로 이용될 수 있다. [참조]
public data class CoroutineName( val name: String ) : AbstractCoroutineContextElement(CoroutineName) { public companion object Key : CoroutineContext.Key<CoroutineName> override fun toString(): String = "CoroutineName($name)" }
Adding contexts
fun main() { val ctx1: CoroutineContext = CoroutineName("Name1") println(ctx1[CoroutineName]?.name) // Name1 println(ctx1[Job]?.isActive) // null println() val ctx2: CoroutineContext = Job() println(ctx2[CoroutineName]?.name) // null println(ctx2[Job]?.isActive) // true println() val ctx3 = ctx1 + ctx2 println(ctx3[CoroutineName]?.name) // Name1 println(ctx3[Job]?.isActive) // true println() val ctx4: CoroutineContext = CoroutineName("Name2") val ctx5 = ctx3 + ctx4 println(ctx5[CoroutineName]?.name) // Name2 println(ctx5[Job]?.isActive) // true println() println(ctx3[CoroutineName]?.name) // Name1 println(ctx3[Job]?.isActive) // true }
- CoroutineContext는 Key가 다른 CoroutineContext와 쉽게 합칠 수 있다. 아래 예시와 같이 plus operator를 구현하고 있기 때문에 손쉽게 두 컨텍스트를 합칠 수 있다.
- 만약 이미 존재하는 키를 합치게 되면 기존의 element를 새로운 element로 대체한다.
- 위의 예시에서 중복된 CoroutineName을 add 한 ctx5는 새로운 element를 대체하여 Name2를 출력하고 있고, add하기 전인 ctx3은 여전히 Name1을 출력하고 있다.
Subtracting elements
- Element는 Key와 minusKey() 함수를 이용하여 context로부터 제거할 수도 있다.
fun main() { val ctx = CoroutineName("Name1") + Job() println(ctx[CoroutineName]?.name) // Name1 println(ctx[Job]?.isActive) // true val ctx2 = ctx.minusKey(CoroutineName) println(ctx[CoroutineName]?.name) // null println(ctx[Job]?.isActive) // true }
Folding context
- 컨텍스트 내에 있는 element들에 특정한 작업이 필요하다면 fold() 메서드를 이용하면 된다. 이는 collection의 fold() 함수와 유사하다.
fun main() { val ctx = CoroutineName("Name1") + Job() ctx.fold("") { acc, element -> "$acc$element" } .also(::println) // CoroutineName(Name1)JobImpl{Active}@57e1b0c val empty = emptyList<CoroutineContext>() ctx.fold(empty) { acc, element -> acc + element } .joinToString() .also(::println) // CoroutineName(Name1), JobImpl{Active}@57e1b0c }
Coroutine context and builders
- CoroutineContext는 자신의 컨텍스트를 자식에게 전파한다. 즉, 자식 컨텍스트는 부모로부터 컨텍스트를 상속받는다.
fun CoroutineScope.log(msg: String) { val name = coroutineContext[CoroutineName]?.name println("[$name] $msg") } fun main() = runBlocking(CoroutineName("main")) { log("Started") val v1 = async { delay(1000) log("Running async") 42 } launch { delay(3000) log("Running launch") } log("The answer is ${v1.await()}") // [main] The answer is 42 } // [main] Started // [main] Running async // [main] The answer is 42 // [main] Running launch
- 자식 컨텍스트는 컨텍스트 선언 시, argument를 전달 받아서 부모의 컨텍스트를 덮어쓸 수 있다.
private fun CoroutineScope.log(msg: String) { val name = coroutineContext[CoroutineName]?.name println("[$name] $msg") } fun main() = runBlocking(CoroutineName("main")) { log("Started") val v1 = async(CoroutineName("C1")) { delay(1000) log("Running async") // [C1] Running async 42 } launch(CoroutineName("C2")) { delay(3000) log("Running launch") // [C2] Running launch } log("The answer is ${v1.await()}") // [main] The answer is 42 } // [main] Started // [C1] Running async // [main] The answer is 42 // [C2] Running launch
Accessing
suspend fun printName() { println(coroutineContext[CoroutineName]?.name) } suspend fun main() = withContext(CoroutineName("Outer")) { printName() // Outer launch(CoroutineName("Innter")) { printName() // Inner } delay(2000) printName() // Outer }
- CoroutineScope는 coroutineContext를 프로퍼티를 가지고 있다.
- 해당 컨텍스트는 continuations에 의해 참조되고 있고, Continuation은 일시중단 함수마다 마지막 파라미터로 넘겨진다.
- 따라서 일시중단 함수 내에서 부모 컨텍스트에 접근할 수 있다.
참고자료
https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837
반응형'Java > Kotlin' 카테고리의 다른 글
[Coroutine] 코루틴 학습 - 7 (Cancellation) (0) 2022.05.03 [Coroutine] 코루틴 학습 - 6 (Job and children awaiting) (0) 2022.05.01 [Coroutine] 코루틴 학습 - 4 (Structured Concurrency) (0) 2022.04.27 [Coroutine] 코루틴 학습 - 3 (Coroutine Builders) (0) 2022.04.27 [Coroutine] 코루틴 학습 - 2 (Under the hood) (0) 2022.04.23