코루틴(Coroutine)
코루틴은 Kotlin에서 지원하는 비동기 처리 기술이다.
코루틴은 멀티 스레딩 문제를 간소화 된 비동기 작업 방식으로 처리하기 위해 개발되었으며,
스레드 내 Context switching 없이 여러 코루틴을 실행, 중단, 재개하는 상호작용을 통해 병행성(동시성)을 갖기에 스레드와 메모리 사용이 줄어들고 개발자가 직접 작업을 스케줄링 할 수 있도록 한다.
즉, 코루틴은 스레드가 아닌 스레드 내에서 동작하는 작업 방식이다.
장점
- 경량( Lightweight ): 코루틴은 실행 중인 스레드를 차단하지 않는 정지(suspend)를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있고, 동시 작업을 진행하면서 차단보다 메모리를 절약할 수 있다.
suspend fun func()
- 메모리 누수 감소: 구조화된 동시 실행을 사용하여 범위 내에서 작업을 실행한다.
- CoroutineScope에서만 새 코루틴을 시작할 수 있다.
- 기본 제공 취소 지원: 취소는 실행 중인 코루틴 계층 구조를 통해 자동으로 전파된다.
- 사용하지 않는 코루틴을 끝낼 수 있는 기능이다.
- Jetpack 통합: 많은 Jetpack 라이브러리에 코루틴을 완전히 지원하는 확장 프로그램이 포함되어 있고, 일부 라이브러리는 자체 코루틴 범위도 제공한다.
- Jetpack에서 코루틴 사용을 밀어주고 있음
Coroutine 특징
1. CPS( Continuation Passing Style )
CPS는 Continuation을 통해 작업을 제어하는 방식이다.
/**
* 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>)
}
fun main() {
println(1) // 1
val continuation = Continuation<Unit>(Dispatchers.IO) { result ->
println("Continuation")
}
println(2) // 2
continuation.resume(Unit) // Continuation
println(3) // 3
}
Continuation의 동작 원리를 간단하게 확인하면 위와 같이 작업을 정의하고 resume을 통해 재개하는 방식으로 동작한다.
자세한 내용은 아래 Suspend Func에서 다루도록 하겠습니다.
2. Structured Concurrent
CoroutineScope는 컨테이너 역할을 한다. 이를 통해 범위를 정의할 수 있고 구조를 정의할 수 있다.
코루틴은 다음과 같은 특징을 갖습니다.
- 부모 코루틴은 자식 코루틴이 모두 끝나야 종료가 된다.
- 부모 코루틴을 취소하면 자식 코루틴까지 취소가 된다.
- 자식 코루틴에서 예외가 발생하면 부모 코루틴까지 예외가 전파된다.
이와 같은 특징으로 비동기 코드를 구조화 할 수 있다.
fun main(){
CoroutineScope(Dispatchers.IO).launch {
launch {
delay(2000)
println(1)
}
launch {
delay(1000)
println("2")
}
println(3)
}
}
Suspend 함수
코루틴을 사용하다 보면 suspend fun 을 자주 사용한다.
suspend fun 함수명()
suspend는 kotlin에서 비동기를 명확하게 구분해주는 키워드가 suspend 이다.
실제로 suspend fun을 java code로 변환시켜보면 Continuation 타입을 인자로 전달 받는다.
@Nullable
public static final Object 함수명(@NotNull Continuation $completion){...}
suspendCoroutine
suspendCoroutine을 사용하면 작업이 정지되고 전달 받은 continuation을 사용하여 resume을 해주면 작업이 재개된다.
class SuspendFunc{
suspend fun suspendFunc(){
println("Do suspendFunc")
println(1)
suspendCoroutine<Unit> { continuation ->
// 정지
firstWork(continuation)
}
println(2)
}
private fun firstWork(continuation: Continuation<Unit>){
thread {
Thread.sleep(1000)
println("Do FirstWork")
continuation.resume(Unit) // 재개
}
}
}
이를 사용하는 대표적인 메서드는 delay()이다.
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
Final State Machine
suspend 함수를 컴파일러는 어떻게 처리를 할까?
컴파일 과정에서 suspend 함수, 중단점에 대해 label로 마킹을 하게 되고, 이를 switch case 문으로 분기하여 처리한다.
예를 들어 다음과 같은 코드가 있다고 가정해보자.
suspend fun doSomething(){
val a = a()
val b = b(a)
}
suspend fun a(): Any {...}
suspend fun b(){...}
해당 코드는 컴파일러에 의해 다음과 같이 변환됩니다.
(정확한 코드는 아니며 이해를 위한 코드라고 보시면 좋을 것 같습니다.)
class CustomContinuation: Continuation<Unit>{
val label: Int = 0,
val result: Any,
override fun resumeWith(result: Result<Any>){
this.result = result
doSomething(this)
}
}
fun doSomething(continuation: Continuation<Unit>){
val cont = continuation as CustomContinuation
when(cont.label){
0 -> {
cont.label = 1
val a = a()
cont.resumeWith(Result.success(a))
}
1 -> {
val a = cont.result as Result<Any>
val b = b(a)
}
}
}
동작 과정은 다음과 같습니다.
1. label 0번
2. label을 1로 변경 후 작업 결과를 resumeWith로 넘겨준다.
3. cont.label 값이 1이 된 상태에서 다시 실행
4. 반복
위처럼 중단점을 라벨링하여 라벨을 switch-case문과 Continuation 객체를 통해 작업을 수행한다.
코루틴의 구성 요소
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 (0) | 2024.03.30 |
---|---|
[안드로이드] 동기와 비동기 (0) | 2024.03.10 |