코루틴 공식 가이드 자세히 읽기 — Part 4 — Dive 1

중단 함수의 비동기 처리를 위한 async { } 코루틴 빌더는 어떻게 구현되어 있을까?

Myungpyo Shim
9 min readMar 1, 2020

Korean [English]

(목차로 돌아가기)

우리가 어떤 중단 함수 A 를 호출하기 위해서는 호출 시점에 임의의 코루틴 스코프 { } 에 위치하거나, 다른 중단 함수 X 안에서 호출 되어야 합니다.

“코루틴 스코프” 는 “코루틴 컨텍스트” 를 갖는 인터페이스 입니다. 코루틴 컨텍스트는 코루틴 이름, 실행 스레드, 예외 핸들러 등이 정의 된 코루틴의 실행 환경 정보 생각해도 좋습니다. 결국, 코루틴 스코프 안에서 일어나는 모든 일들은 코루틴 컨텍스트에 정의 된 정보 기반으로 일어나게 됩니다.

코루틴은 기본적으로 코루틴 스코프를 구현합니다. 그래서 우리는 임의의 코루틴 안에서 (즉, 코루틴 스코프 안에서) 중단 함수들을 호출 할 수 있습니다.

그러면 중단 함수를 실행하기 위한 이러한 코루틴은 어떻게 생성할까요? 대표적인 코루틴 빌더로는 launch { } 와 async { } 코루틴 빌더가 있습니다. 이번에는 이 두 코루틴 빌더의 구현을 비교해 보면서 async { } 코루틴 빌더의 특징에 대해 알아보겠습니다.

먼저 launch { } 코루틴 빌더의 함수 시그니처는 다음과 같습니다.

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job

그리고 async { } 코루틴 빌더는 다음과 같습니다.

public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>

두 빌더 함수에 전달되는 인자들 중 context 와 start 는 동일합니다. 하지만 그 역할에 대해 조금만 알아보자면,
context 는 EmptyCoroutineContext 가 기본 값으로 설정 되어 있는데, CoroutineName 이나 Dispatcher 등이 정의 된 컨텍스트를 전달하면 async { } 를 호출 한 곳(parent) 의 context 와 병합하여 새로운 코루틴을 위한 컨텍스트를 만들어 냅니다.
start 는 코루틴을 시작시키는 방식을 나타내는데 DEFAULT, LAZY, ATOMIC, UNDISPATCHED 가 있습니다.

두 빌더 함수가 차이를 보이는 부분은 두 부분입니다.

  • 코루틴 빌더의 마지막 전달 인자인 block 이 launch { } 빌더는 Unit 이고, async { } 빌더는 실행 결과 타입(T) 입니다.
  • 코루틴 빌더의 반환 타입이 launch { } 빌더는 Job 이고, async { } 빌더는 지연 결과 타입(Deferred<T>) 입니다.

이 두 가지 차이점을 보면 두 빌더 함수의 역할을 미루어 짐작해 볼 수 있습니다.

launch { } 빌더는 코루틴을 실행하고 실행 결과를 받을 필요 가 없는 로직일 경우 (fire & forget) 사용하고, async { } 빌더는 실행한 코루틴의 결과가 필요한 경우 사용 합니다.

또한 launch { } 빌더의 실행 결과로 받은 Job 객체를 이용하여 실행중인 코루틴을 취소할 수 있는데, Deferred<T> 는 Job 인터페이스를 상속하므로 동일하게 코루틴을 취소할 수 있으며 await() 함수를 제공하여 코루틴의 실행 결과를 받을 수 있습니다.

이제 두 빌더의 함수 구현부를 살펴봅시다.

launch { } 코루틴 빌더

async { } 코루틴 빌더

두 함수의 구현부 중 차이가 나는 부분은 7번 라인에서 시작하는 실행할 코루틴을 생성하는 부분인데, 빌더에 전달 된 CoroutineStart 인자에 따라서 일 경서로 다른 타입의 코루틴을 생성하고 있는 부분입니다.

start: CoroutineStart 인자의 기본 값은 CoroutineStart.DEFAULT 이고 CoroutineStart.LAZY 가 전달되면 Lazy 가 prefix 로 붙은 지연 실행을 지원하는 코루틴을 생성합니다. 이렇게 생성된 코루틴은 직접 start() 함수를 호출해 줘야 시작됩니다. 지연 실행을 위한 코루틴 타입이 다르긴 하지만 지연 실행을 지원한다는 점에서는 두 빌더가 동일합니다.

결국 차이점은 StandAloneCoroutine 과 DeferredCoroutine<T> 의 구현에 있다고 할 수 있습니다.

StandAloneCoroutine 의 생성자는 다음과 같고

private open class StandaloneCoroutine(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<Unit>(parentContext, active)

DeferredCoroutine<T> 의 생성자는 다음과 같습니다.

private open class DeferredCoroutine<T>(
parentContext: CoroutineContext,
active: Boolean
) : AbstractCoroutine<T>(parentContext, active), Deferred<T>, SelectClause1<T>

두 코루틴 모두 템플릿으로 반환 타입을 정의하는 AbstractCoroutine 을 상속하고 있으며 StandaloneCoroutine 은 반환 값이 없음을 나타내는 타입 Unit 을 사용하고 DeferredCoroutine 은 반환 값의 타입인 T 를 그대로 사용합니다.

DeferredCoroutine 은 추가로 Deferred<T>를 구현하는데 Deferred<T> 를 구현함으로써 await() 함수를 호출 하는 쪽에 제공하여 await() 호출 시 코루틴이 시작되지 않았으면 시작 시키고, 시작 되었다면 종료될 때까지 기다리는 중단함수를 실행하게 됩니다.

DeferredCoroutine 은 SelectClause1<T> 또한 구현하는데 SelectClause1 은 다른 챕터에서 다룬 Select 구문에서 Deferred 값을 이용할 때 사용되는 부분으로 다른 파트에서 알아보겠습니다.

그럼 이제 AbstractCoroutine 의 생성자를 살펴봅시다.

public abstract class AbstractCoroutine<in T>(
@JvmField
protected val parentContext: CoroutineContext,
active: Boolean = true
) : JobSupport(active), Job, Continuation<T>, CoroutineScope

AbstractCoroutine 은 생성자의 인자로 새로 생성되는 코루틴이 실행될 부모 컨텍스트와 새로 생성된 Job 이 활성 상태로 시작할지를 나타내는 값(active)을 전달 받습니다. active 는 코루틴의 스테이스 머신 관리 객체인 JobSupport 로 전달되어 초기 상태를 설정하게 됩니다.

AbstractCoroutine 은 Job 인터페이스를 구현하는데 위 launch { } 빌더 함수를 보면 결국 생성 된 코루틴을 이 Job 타입으로(upcasting) 반환하고 있음을 알 수 있습니다. 이를 이용해서 우리는 실행된 코루틴을 취소할 수 있게 됩니다.

async { } 빌더 함수는 Deferred<T> 를 반환하고 있는데 위에서 본 것 처럼 AbstractCoroutine 을 상속하는 DeferredCoroutine<T> 가 Deferred<T> 인터페이스를 구현하며 이 타입으로 반환함으로써 반환 값에 접근할 수 있도록 해 주는 것입니다.

Deferred<T> 인터페이스는 다음과 같이 await() 이라는 하나의 중단 함수만 정의하고 있으며

public suspend fun await(): T

이 구현은 AbstractCoroutine 이 상속한 JobSupport 에서 다음과 같이 구현되어 있습니다.

5 번 라인의 InComplete 이 아닌 경우, 즉 코루틴 호출 쪽에서 await() 을 호출 한 시점에 이미 코루틴의 실행이 끝난 상태라면 빠르게 성공 시 반환 값을 실패 시 예외를 던지도록 작성 된 부분입니다.

13 번 라인은 코루틴을 시작시키기 위한 함수를 호출하는 부분이며 state ≥ 0 의 의미는 코루틴 시작 요청이 성공했다는 의미입니다. 요청이 성공했다는 것은 코루틴을 성공적으로 시작시켰거나(TRUE), 이미 코루틴이 시작되어 있어서 시작시키지 않았다는 것(FALSE)을 의미하며 이 경우는 바로 루프를 빠져나가 반환 값을 받기 위한 코드를 실행하게 됩니다. 그 이외의 상태는 재시도(RETRY) 가 있는데 이것은 상태 변경을 위한 동기화 과정에서 기존 상태가 다른 스레드에 의해 변경되어 다시 시작 요청이 필요할 경우를 나타내며 이경우는 다시 루프를 돌며 시작요청을 하게 됩니다.

마지막에 호출하는 awaitSuspend() 중단 함수는 호출 시 대상 코루틴(작업을 실행중인)이 종료되지 않았으면 현재 코루틴을 중단시키고 결과를 대기하고, 이미 완료되었다면 결과 값을 수신 받는 역할을 수행합니다.

정리해 보자면 지금까지 등장했던 코루틴 계층 구조는 다음과 같습니다.

Job
— JobSupport
— — AbstractCoroutine<T>
— — — StandaloneCoroutine
— — — — LazyStandaloneCoroutine
— — — DeferredCoroutine<T>
— — — — LazyDeferredCoroutine<T>

async { } 코루틴 빌더는 launch { } 빌더와 동일하게 AbstractCoroutine<T> 를 상속하며 T 는 반환 타입을 의미하고, async { } 코루틴 빌더로 생성되는 코루틴은 DeferredCoroutine<T> 이며 이는 Deferred<T> 인터페이스를 구현 합니다. Deferred 인터페이스에서 정의하면 await() 의 실제 구현부는 JobSupport 클래스에 있으며 Lazy 코루틴의 시작, 실행중인 코루틴을 위한 대기, 실행 종료된 코루틴의 결과 확인의 역할을 수행합니다.

끝.

--

--