코루틴(Coroutine)
코루틴은 Kotlin에서 지원하는 비동기 처리 기술이다.
코루틴은 멀티 스레딩 문제를 간소화 된 비동기 작업 방식으로 처리하기 위해 개발되었으며,
스레드 내 Context switching 없이 여러 코루틴을 실행, 중단, 재개하는 상호작용을 통해 병행성(동시성)을 갖기에 스레드와 메모리 사용이 줄어들고 개발자가 직접 작업을 스케줄링 할 수 있도록 한다.
suspend fun test(){
...
// 중단
suspendCoroutine<Unit>{ continuation: Continuation<Unit> ->
continuation.resume(Unit) // 재개
}
...
}
/**
* Interface representing a continuation after a suspension point that returns a value of type `T`.
*/
@SinceKotlin("1.3")
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext
/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}
즉, 코루틴은 스레드가 아닌 스레드 내에서 동작하는 작업 방식이다.
장점
- 경량( Lightweight ): 코루틴은 실행 중인 스레드를 차단하지 않는 정지(suspend)를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있고, 동시 작업을 진행하면서 차단보다 메모리를 절약할 수 있다.
suspend fun func()
- 메모리 누수 감소: 구조화된 동시 실행을 사용하여 범위 내에서 작업을 실행한다.
- CoroutineScope에서만 새 코루틴을 시작할 수 있다.
- 기본 제공 취소 지원: 취소는 실행 중인 코루틴 계층 구조를 통해 자동으로 전파된다.
- 사용하지 않는 코루틴을 끝낼 수 있는 기능이다.
- Jetpack 통합: 많은 Jetpack 라이브러리에 코루틴을 완전히 지원하는 확장 프로그램이 포함되어 있고, 일부 라이브러리는 자체 코루틴 범위도 제공한다.
- Jetpack에서 코루틴 사용을 밀어주고 있음
Suspend 함수
suspend fun 함수명()
코루틴을 사용하다 보면 suspend fun 을 자주 사용한다.
suspend는 kotlin에서 비동기를 명확하게 구분해주는 키워드가 suspend 이다.
실제로 suspend fun을 java code로 변환시켜보면 Continuation 타입을 인자로 전달 받는다.
@Nullable
public static final Object 함수명(@NotNull Continuation $completion){...}
코루틴의 구성 요소
CoroutineScope
하나 이상의 관련 코루틴을 관리, 모든 코루틴은 해당 범위 내에서 실행해야 한다.
- 종류: CoroutineScope, withContext, LifecycleScope, ViewModelScope…
- withContext : CoroutineContext를 override 할 수 있다.
launch (Builder)
코루틴을 만들고 함수 본문의 실행을 해당하는 Dispatcher에 전달하는 함수
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
}
- 따로 Dispatcher를 설정해주지 **메인 스레드**에서 동작한다.
CoroutineContext
- Job
- CoroutineName
- CoroutineExceptionHandler
- val exceptionHandler = CoroutineExceptionHandler { coroutineContext, e -> when(e){ is NetworkException -> {} ... else -> throw e } }
- CoroutineDispatcher: 코루틴이 어느 스레드에서 실행될지 결정하는 역할
- Dispatchers.Main : UI와 상호작용하고 빠른 작업을 실행하기 위해서만 사용해야 한다.
- Dispatchers.IO : 메인 스레드 외부에서 디스크 또는 네트워크 I/O를 실행하도록 최적화되어 있다.
- Dispatchers.Default : CPU를 많이 사용하는 작업을 실행하는데 최적화되어 있다. (list sort, Json parsing..)
- Dispatchers.Main : Handler를 통해 Queue에 작업을 쌓는 방식으로 즉시 수행이 될 것이라는 보장이 없다.
코루틴 예외
코루틴에서 예외가 발생하면 예외는 가지처럼 퍼져나가 자식 및 부모 코루틴 모두 Cancel 된다.
SupervisorJob
SupervisorJob은 이러한 예외에 대해 부모 코루틴으로 전파되지 않도록 제한하는 Job이다.
internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob {
public open fun childCancelled(cause: Throwable): Boolean {
if (cause is CancellationException) return true
return cancelImpl(cause) && handlesException
}
}
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
override fun childCancelled(cause: Throwable): Boolean = false
}
// 부모에게 전달하는 코드
private fun cancelParent(cause: Throwable): Boolean {
// Is scoped coroutine -- don't propagate, will be rethrown
if (isScopedCoroutine) return true
/* CancellationException is considered "normal" and parent usually is not cancelled when child produces it.
* This allow parent to cancel its children (normally) without being cancelled itself, unless
* child crashes and produce some other exception during its completion.
*/
val isCancellation = cause is CancellationException
val parent = parentHandle
// No parent -- ignore CE, report other exceptions.
if (parent === null || parent === NonDisposableHandle) {
return isCancellation
}
// Notify parent but don't forget to check cancellation
return parent.childCancelled(cause) || isCancellation
}
위의 코드에서 자식의 Cancel 여부를 전송할 지를 childCancelled()에서 결정한다.
JobImpl에서는 예외 발생 시 true를, SupervisorJobImpl에서는 false를 반환한다.
이 정보를 가지고 cancelParent()에서 부모에게 전달 할 지를 결정하는데, 마지막 반환 값을 보면 childCancelled() 의 반환 값을 리턴한다.
return parent.childCancelled(cause) || isCancellation
따라서 SupervisorJob은 false를 반환하기 때문에 부모에게 전파되지 않고 예외가 발생한 자식만 취소된다는 것을 알 수 있다.
CancellationException : Job이 Cancel 될 때 발생
Android CoroutineScope
1. LifeCycleScope
LifeCycleScope는 LifecycleOwner 에 종속되어 있는 Coroutine Scope이다.
// LifecycleOwner.kt
public interface LifecycleOwner {
public val lifecycle: Lifecycle
}
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
// Lifecycle.kt
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (internalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}
LifecycleOwner에 종속되어 있다 보니 Lifecycle이 종료되면 자동으로 LifecycleScope 또한 cancel된다. 따라서 Lifecycle에 따른 CoroutineScope를 관리 할 필요가 없다는 장점이 있다.
internal class LifecycleCoroutineScopeImpl(
override val lifecycle: Lifecycle,
override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
init {
// in case we are initialized on a non-main thread, make a best effort check before
// we return the scope. This is not sync but if developer is launching on a non-main
// dispatcher, they cannot be 100% sure anyways.
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
coroutineContext.cancel()
}
}
fun register() {
launch(Dispatchers.Main.immediate) {
if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
} else {
coroutineContext.cancel()
}
}
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
lifecycle.removeObserver(this)
coroutineContext.cancel()
}
}
}
2. viewModelScope
viewModelScope는 ViewModel() 내에서만 사용할 수 있는 Coroutine Scope이다.
ViewModel이 onCleared로 사라질 때 자동으로 내부의 Coroutine Scope 또한 cancel 된다.
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
'안드로이드 > Asynchronous' 카테고리의 다른 글
[안드로이드] Coroutine Flow (1) | 2024.03.30 |
---|---|
[안드로이드] 스레드 (0) | 2024.03.11 |
[안드로이드] 동기와 비동기 (0) | 2024.03.10 |