Coroutine withContext + NonCancellable
⭐
본 글은 withContext 코루틴 빌더가 하는 일은 무엇인가?을 읽고 공부한 내용을 정리하는 글입니다.
지난 내용
이전 글에서 중단된 코루틴의 finally { }
블록에서 중단 함수를 사용할 때 withContext를 사용하는 것에 대해 다뤘다. 간단히 살펴보면 다음과 같다.
fun main(args: Array<String>): Unit = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("$i")
delay(500L)
}
} finally {
withContext(NonCancellable) {
delay(1000L) // <-- 중단 함수
println("실행 완료")
}
}
}
delay(1300L)
println("중단 시도")
job.cancelAndJoin()
println("종료")
}
cancelAndJoin()
으로 job이 취소되면 try { } 블록에서 CancelledException이 발생하면서 finally { } 블럭으로 진입하게 된다. 이때 withContext 없이 중단 함수인 delay()를 호출하면 현재 코루틴 컨텍스트는 이미 취소된 상태이기 때문에 CancelledException이 다시 발생한다.
이를 withContext { } 와 NonCancellable 컨텍스트 요소의 조합으로 해결할 수 있었다.
withContext의 반환
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
// compute new context
val oldContext = uCont.context
val newContext = oldContext + context
// always check for cancellation of new context
newContext.ensureActive()
// FAST PATH #1 -- new context is the same as the old one
if (newContext === oldContext) {
val coroutine = ScopeCoroutine(newContext, uCont)
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
// FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
// `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
val coroutine = UndispatchedCoroutine(newContext, uCont)
// There are changes in the context, so this thread needs to be updated
withCoroutineContext(newContext, null) {
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
// SLOW PATH -- use new dispatcher
val coroutine = DispatchedCoroutine(newContext, uCont)
block.startCoroutineCancellable(coroutine, coroutine)
coroutine.getResult()
}
}
withContext 함수는 로직이 담긴 코드 블록과 이것이 실행될 코루틴 컨텍스트를 매개 변수로 받는다.
@kotlin.SinceKotlin @kotlin.internal.InlineOnly public suspend inline fun <T> suspendCoroutineUninterceptedOrReturn(crossinline block: (kotlin.coroutines.Continuation<T>) -> kotlin.Any?): T { contract { /* compiled contract */ }; /* compiled code */ }
그리고 suspendCoroutineUninterceptedOrReturn() 함수를 반환하는데 이것은 전달된 코드 블럭에서 호출 코루틴(Continuation) 정보에 접근할 수 있도록 해준다. 전달된 코드 블록에서 COROUTINE_SUSPENDED 라는 사전 정의 된 상수 값을 반환 할 경우 코루틴이 처리를 위해 시간이 필요하며 즉시 값을 반환하지 않고 처리가 완료되면 continuation 파라미터를 통해 결과를 전달할 것을 나타내며, 그 이외의 값을 반환할 경우 중단 없이 바로 결과 값을 반환할 것을 나타낸다.
withContext 3가지 방식
withContext 함수로 전달된 컨텍스트에 따라 3가지 방식으로 처리가 된다.
- Function Caller - Callee 코루틴 컨텍스트가 완전히 동일한 경우 (===)
- 현재 컨텍스트에서 그대로 코드 블럭을 실행해도 무방하기 때문에 ScopeCoroutine을 만들어 바로 실행한다. ScopeCoroutine은 코루틴 간 컨텍스트가 동일해서 바로 작업을 수행할 수 있을 경우 사용됨
- Function Caller - Callee 코루틴 컨텍스트가 서로 다른 부분이 있지만 Dispatcher 컨텍스트 요소는 동일한 경우 (==)
- UndispatchedCoroutine을 만들고 새로 만들어진 컨텍스트 안에서 실행 (withContext)
- 그 외의 경우
- 코루틴 컨텍스트 간에 dispatcher를 포함한 변경 사항이 있는 것이기 때문에 새로운 컨텍스트를 이용해 적절한 스레드로 dispatch 되어 실행될 수 있도록 DispathcedCoroutine을 생성해서 실행
NonCancellable
지난 내용에서 NonCancellable을 withContext로 넘기는 부분이 있었다. NonCancellable은 어떻게 취소된 코루틴 블록에서 예외를 발생시키지 않고 작업을 이어나갈 수 있을까?
내부를 살펴보자.
public object NonCancellable : AbstractCoroutineContextElement(Job), Job {
private const val message = "NonCancellable can be used only as an argument for 'withContext', direct usages of its API are prohibited"
/**
* Always returns `true`.
* @suppress **This an internal API and should not be used from general code.**
*/
@Deprecated(level = DeprecationLevel.WARNING, message = message)
override val isActive: Boolean
get() = true
/**
* Always returns `false`.
* @suppress **This an internal API and should not be used from general code.**
*/
@Deprecated(level = DeprecationLevel.WARNING, message = message)
override val isCompleted: Boolean get() = false
/**
* Always returns `false`.
* @suppress **This an internal API and should not be used from general code.**
*/
@Deprecated(level = DeprecationLevel.WARNING, message = message)
override val isCancelled: Boolean get() = false
// ...
NonCancellable은 AbstractCoroutineContextElement를 상속하는 컨텍스트 요소이다. 작업을 이어나갈 수 있는 이유는 isActive가 항상 true를 반환하기 때문이다.
그렇다면 withContext(NonCancellable)은 withContext 3가지 방식 중 어느 방식으로 진행될까?
정답은 두 번째 경로이다.
코드에서 oldContext는 현재 코루틴의 컨텍스트이고, newContext는 oldContext + NonCancellable 이다. NonCancellable에는 dispatcher 컨텍스트 요소에 대한 정의가 들어있지 않으므로 둘을 plus 하게 되면 oldContext의 dispatcher 요소를 상속하여 사용하게 된다.
NonCancellable은 withContext 에서만 사용되어야 한다.