[코루틴에 관한 정리] 2. Coroutine Scope와 Coroutine Context

1
post-thumbnail

오늘 알아볼 내용은 Coroutine ContextCoroutine Scope에 대한 내용입니다.

우리는 Coroutine을 사용할때 아래와 같이 사용합니다.

CoroutineScope(Dispatchers.Main).launch {
	// Main Thread
}

위의 코드를 3개로 분리해보면,

  • CoroutineScope
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())
  • launch()
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
}
  • CoroutineContext로 나눌수 있습니다.

한번 차근차근 밟아볼까요?

1. Coroutine Context

Coroutine Context의 내부를 확인해 봅시다.

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>

Coroutine Context는 interface로 이루어져 있습니다.

여기서 plus() 연산자를 확인해봅시다.

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)
                }
            }
        }

Context가 텅빈 코루틴 Context일 경우, 파라미터로 받은 CoroutineContext를 호출하여 최적화를 하고, 아닐경우 누적함수를 사용합니다.
그리고 현재 Coroutine Context에서 해당 요소(Element)들을 제거하고, 받은 새로운 값으로 덮어 씌웁니다.

Coroutine Context의 구현체는 총 3 가지가 있습니다.

  • EmptyCoroutineContext : 컨텍스트가 명시되지 않을때 이러한 객체를 사용합니다.
  • CombinedContext : 두개 이상의 컨텍스트가 명시되면 컨텍스트 간 연결
  • Element : 컨텍스트의 각요소입니다.

Element들이 서로 연결될때, CombinedContext가 Container역할을 하게됩니다.

Interface인 Element의 구현체는 아래와같은것들이 존재합니다.

  • CoroutineId
  • CoroutineName
  • CoroutineDispatcher
  • ContinuationInterCeptor
  • CoroutineExceptionHandler

이 Coroutine Context를 상속받은 Element는 지역변수로 key를 가지고 있습니다.
이 키를 기반으로 Element들은 CoroutineContext에 등록됩니다.
(Coroutine Context는 마치 Map 처럼 작동을 합니다.)

// 
    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를 디버깅해보면, element의 집합이라는것을 알수 있습니다.

2. CoroutineScope

CoroutineScope는 coroutineContext를 멤버변수를 가진 단순한 interface 입니다.

public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}

우리가 CoroutineScope()를 하게 실행되는건 아래의 메소드입니다.

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

해당 메소드는 주어진 context 에 Job Element가 없으면 기본 Job() 이 생성됩니다.

3. Job()

public interface Job : CoroutineContext.Element {
    public val children: Sequence<Job>
}

Job()또한 Element를 상속한 Interface 입니다.
내부변수로 children을 갖고있는데, 안에 갖고있는건 자식 Context의 Job()입니다.

Coroutine의 추상클래스인 AbstractCroutine에서는 ParentContext를 멤버변수를 갖고있습니다.
또한 안의 initParentJob을 통해 자식 관계를 설정합니다.


Job의 실질적인 구현체인 JobSupport입니다.

여기서 자식이 죽으면 어떻게 상위로 올라가는지 cancelParent() 를 통해 찾아볼 수 있습니다.

cancelParent() 함수는 현재 코루틴에서 발생한 예외를 부모 코루틴으로 전달함으로써 예외 처리를 요청하는 함수입니다.
이 함수가 true를 반환한다면 전달 된 예외는 부모 코루틴에의해서 처리된다는 의미이고,
false를 반환한다면 해당 예외는 부모에 의해 처리될 수 없으니 현재 코루틴이 처리해야 한다는 것을 의미합니다.

    /**
     * The method that is invoked when the job is cancelled to possibly propagate cancellation to the parent.
     * Returns `true` if the parent is responsible for handling the exception, `false` otherwise.
     *
     * Invariant: never returns `false` for instances of [CancellationException], otherwise such exception
     * may leak to the [CoroutineExceptionHandler].
     */
    private fun cancelParent(cause: Throwable): Boolean {
        // 스코프드 코루틴일 경우, 항상 상위로 올립니다.
        if (isScopedCoroutine) return true

        /*CancellationException은 "정상"으로 간주되며 일반적으로 부모는 자식이 생성할 때 취소되지 않습니다. 
        이렇게 하면 자식이 충돌하고 완료 중에 다른 예외를 생성하지 않는 한 부모가 취소되지 않고 
        자식을 취소할 수 있습니다(일반적으로).
         */
        val isCancellation = cause is CancellationException
        val parent = parentHandle
       
       //Parent가 없거나 연결이 안되어 있다면(부모 코루틴일 경우)
        if (parent === null || parent === NonDisposableHandle) {
        	//캔슬 -> 부모로 올림 , 캔슬아님 -> 이곳에서 처리(부모이기 때문에)
            return isCancellation
        }
        
        // 스코프드 코루틴이나 부모관계가 있을경우,
        // 부모한테 예외를 전파합니다.
        return parent.childCancelled(cause) || isCancellation
    }

cancelParent가 사용되는 finalizeFinishingState에서는
cancelParent() 가 false일경우, 다시 말해서 이곳에서 예외를 처리해야할 경우 handle JobException을 일으킵니다.

이렇게 Job()을 통해 자식의 에러가 상위에게 전파하는데 사용됩니다.


참고

Developer/Kotlin & JavaKotlin Coroutine - 3. 코루틴의 Flow 활용

코루틴 공식 가이드 읽고 분석하기— Part 1 — Dive1

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글