오늘 알아볼 내용은 Coroutine Context와 Coroutine Scope에 대한 내용입니다.
우리는 Coroutine을 사용할때 아래와 같이 사용합니다.
CoroutineScope(Dispatchers.Main).launch {
// Main Thread
}
위의 코드를 3개로 분리해보면,
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
}
한번 차근차근 밟아볼까요?
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 가지가 있습니다.
Element들이 서로 연결될때, CombinedContext가 Container
역할을 하게됩니다.
Interface인 Element의 구현체는 아래와같은것들이 존재합니다.
이 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의 집합이라는것을 알수 있습니다.
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() 이 생성됩니다.
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()을 통해 자식의 에러가 상위에게 전파하는데 사용됩니다.