본문 바로가기

안드로이드/Asynchronous

[안드로이드] 코루틴

코루틴(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..)
    비동기가 아닌 동기로 실행되어야 하는 작업은 (UI Update) Dispatchers.Main 보단 Dispatchers.Main. Immediate 사용을 권장한다.Dispatchers.Main. Immediate : Queue에 쌓지 않고 즉시 실행
  • 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