Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Archives
Today
Total
관리 메뉴

Life Engineering

코루틴 컨텍스트 (Coroutine Context) 본문

공부/Kotlin

코루틴 컨텍스트 (Coroutine Context)

흑개 2024. 6. 21. 01:37

Coroutine Context

CoroutineContext는 다음과 같이 코루틴 빌더 함수에 보인다 .. 대체 뭘까?

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

CoroutineContext는 코루틴을 실행하는 실행 환경을 설정하고 관리하는 인터페이스

CoroutineContext 객체는 CoroutineDispatcher, CoroutineName, Job 등의 객체 조합해 코루틴의 실행 환경 설정

@SinceKotlin("1.3")
public interface CoroutineContext {
    /**
     * Returns the element with the given [key] from this context or `null`.
     */
    public operator fun <E : Element> get(key: Key<E>): E?

    /**
     * Accumulates entries of this context starting with [initial] value and applying [operation]
     * from left to right to current accumulator value and each element of this context.
     */
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    /**
     * Returns a context containing elements from this context and elements from  other [context].
     * The elements from this context with the same key as in the other one are dropped.
     */
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

    /**
     * Returns a context containing elements from this context, but without an element with
     * the specified [key].
     */
    public fun minusKey(key: Key<*>): CoroutineContext

    /**
     * Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
     */
    public interface Key<E : Element>

    /**
     * An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
     */
    public interface Element : CoroutineContext {
        /**
         * A key of this coroutine context element.
         */
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }
}

CoroutineContext는 Job, CoroutineNmae, CoroutineDispatcher 와 같은 Element 객체들이 인덱싱된 집합임 (⇒ indexed set)

CoroutineContext 의 구성 요소

  • CoroutineName: 코루틴의 이름 설정
  • CoroutineDispatcher: 코루틴을 스레드에 할당해 실행하는 Dispatcher
  • Job: 코루틴의 추상체로 코루틴을 조작하는데 사용
  • CoroutineExceptionHandler: 코루틴에서 발생한 예외를 처리

위와 같은 CoroutineContext.Element 또한 CoroutineContext 임

즉 CoroutineContext 의 모든 원소가 CoroutineContext 로 되어 있음 → Composite Design Pattern 적용

따라서 컨텍스트의 지정과 변경이 편리함

다음과 같이 element를 더하면 결과적으로 더한 element를 포함한 context 가 생성됨 ..

launch(CoroutineName("Name1")) { ... }
launch(CoroutineName("Name2") + Job()) { ... }

컨텍스트에서 모든 원소는 식별할 수 있는 유일한 Key 를 가지고 있음

CoroutineContext 원소 찾기

data class CoroutineName(
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {

    override fun toString(): String = "CoroutineName($name)"

    companion object Key : CoroutineContext.Key<CoroutineName>
}
interface Job : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<Job>

    // ...
}

CoroutineContext 구성 요소를 보면, 컴패니언 객체인 key 가 있음 → 클래스의 이름이 컴패니언 객체에 대한 참조로 사용될 수 있기 때문에 key 값을 CoroutineName, Job 과 같은 형태로 접근할 수 있음

  • 싱글톤 키를 이용해 CoroutineContext 구성 요소에 접근

컴패니언 객체인 key 는 단일 인스턴스(싱글턴 객체) 이므로 CoroutineName.key 와 같은 형식으로 접근할 수 있음

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
  val coroutineContext = CoroutineName("MyCoroutine") + Dispatchers.IO
  val nameFromContext = coroutineContext[CoroutineName.Key]
  println(nameFromContext)
}
  • 구성 요소 자체를 키로 사용해 접근

컴패니언 객체를 참조할 때 객체를 둘러싼 클래스의 이름을 바로 사용할 수 있는 코틀린 언어의 특성 상, _.key_를 사용하지 않고 구성 요소 자체를 키로 사용할 수 있음

  • 구성 요소의 key 프로퍼티를 사용해 구성 요소에 접근

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
  val coroutineName : CoroutineName = CoroutineName("MyCoroutine")
  val dispatcher : CoroutineDispatcher = Dispatchers.IO
  val coroutineContext = coroutineName + dispatcher

  println(coroutineContext[coroutineName.key]) // CoroutineName("MyCoroutine")
  println(coroutineContext[dispatcher.key]) // Dispatchers.IO
}

이때 coroutineName.key와 CoroutineName.key (컴패니언 객체로 선언된 Key) 와 동일한 객체를 가리킴

CoroutineContext 더하기

  • CoroutineContext 구성 요소 덮어씌우기

    위 코드와 같이 같은 키를 가진 다른 원소가 더해지면 맵처럼 새로운 원소가 기존 원소를 대체함
  • 여러 구성 요소로 이뤄진 CoroutineContext 합치기

    각 Context 를 더해 구성 요소를 합칠 수 있음

Empty 코루틴 컨텍스트

collectioin 처럼 CoroutineContext도 빈 컨텍스트를 가질 수 있음

다른 context를 더하면, 다른 context 처럼 동작함

fun main() {
    val empty: CoroutineContext = EmptyCoroutineContext
    println(empty[CoroutineName]) // null
    println(empty[Job]) // null

    val ctxName = empty + CoroutineName("Name1") + empty
    println(ctxName[CoroutineName]) // CoroutineName(Name1)
}

CoroutineContext 의 구성 요소 제거하기

minusKey 는 구성 요소의 키를 인자로 받아 해당 구성 요소를 제거한 CoroutineContext 객체 반환

minusKey 를 호출한 CoroutineContext 객체는 그대로 유지되고 구성 요소가 제거된 새로운 CoroutineContext 가 반환됨

아래 코드처럼 CoroutineName.Key 를 사용할 자리에 CoroutineName 을 써도 OK

CoroutineContext와 빌더

부모는 기본적으로 컨텍스트를 자식에게 전달

자식은 부모로부터 컨텍스트를 상속받음

위 코드와 같이 자식은 부모로부터 컨텍스트를 전달받음(⇒ 컨텍스트의 코루틴 네임 같음)

log 함수는 CoroutineScope 의 확장 함수이며 CoroutineContext 의 CoroutineName 을 출력하는 함수임

모든 자식은 빌더의 인자에서 정의된 특정 컨텍스트를 가질 수 있음

인자로 전달된 컨텍스트는 부모로부터 상속받은 컨텍스트를 대체

부모 컨텍스트 중 구성요소 CoroutineName을 자식 컨텍스트의 구성요소가 대체함

코루틴 컨텍스트 계산 공식: defaultContext + parentContext + childContext

새로운 원소가 같은 키를 가진 이전 원소를 대체해서 자식의 컨텍스트는 부모 컨텍스트 중 같은 키를 가진 원소를 대체하게 됨

중단 함수에서 Context 접근

컨텍스트는 중단 함수 사이에 전달되는 Continuation 객체가 참조하고 있음

중단 함수에서 부모의 컨텍스트에 접근하는 것이 가능

coroutineContext 프로퍼티는 모든 중단 스코프에서 사용 가능하고 이를 통해 컨텍스트에 접근 가능하다

import kotlinx.coroutines.*

suspend fun printName() = coroutineScope {
    println(coroutineContext[CoroutineName]?.name)
}

fun main() = runBlocking(CoroutineName("Outer")) {
    printName() // Outer
    launch(CoroutineName("Inner")) {
        printName() // Inner
    }
    delay(10)
    printName() // Outer

'공부 > Kotlin' 카테고리의 다른 글

코루틴 디스패처(Coroutine Dispatcher)  (0) 2024.06.20
코루틴 빌더 기초  (0) 2024.06.14
Kotlin 코루틴의 기본  (1) 2024.06.09