코루틴은 그림과 같이 생성, 실행 중, 실행 완료 중, 실행 완료, 취소 중, 취소 완료 상태를 가질 수 있다.
cancel()
함수 등을 통해 코루틴에 취소가 요청되었을 때 취소 중 상태로 넘어간다. 이전 글에도 설명했지만 취소 요청을 했다고 해서 바로 취소되는 것은 아니다.코루틴의 구조화는 큰 작업을 여러 작은 작업으로 나누는 방식으로 이뤄진다.
예를 들어, 여러 DB에서 데이터를 가져와서 합치는 큰 작업(부모 코루틴)이 있다고 가정해보자. DB 1, DB 2, DB 3에서 데이터를 가져오는 작업은 작은 작업(자식 코루틴)이 될 것이다. 만약 이러한 작업에서 자식 코루틴이 완료되기 전에 부모 코루틴이 실행 완료된다면 DB에서 데이터를 정상적으로 가져올 수 없을 것이다.
그래서 부모 코루틴은 모든 자식 코루틴이 실행 완료되어야 완료될 수 있다. 작은 작업이 모두 완료돼야 큰 작업을 완료할 수 있다. 이를 부모 코루틴이 자식 코루틴에 대해 완료 의존성을 가진다고 한다.
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val parentJob = launch { // 부모 코루틴 실행
launch { // 자식 코루틴 실행
delay(1000L) // 1초간 대기
println("[${getElapsedTime(startTime)}] 자식 코루틴 실행 완료")
}
println("[${getElapsedTime(startTime)}] 부모 코루틴이 실행하는 마지막 코드")
}
parentJob.invokeOnCompletion { // 부모 코루틴이 종료될 시 호출되는 콜백 등록
println("[${getElapsedTime(startTime)}] 부모 코루틴 실행 완료")
}
}
/*
// 결과:
[지난 시간: 13ms] 부모 코루틴이 실행하는 마지막 코드
[지난 시간: 1021ms] 자식 코루틴 실행 완료
[지난 시간: 1022ms] 부모 코루틴 실행 완료
*/
fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms"
위 코드를 보면 부모 코루틴의 마지막 코드가 실행되었는데도 부모 코루틴은 종료되지 않고 자식 코루틴이 실행 완료된 이후 부모 코루틴이 실행 완료된다. invokeOnCompletion()
함수는 코루틴이 실행 완료되거나 취소 완료됐을 때 실행되는 콜백을 등록하는 함수이다.
부모 코루틴은 이렇게 마지막 코드를 실행한 후 자식 코루틴의 실행 완료를 기다릴 때까지 실행 완료 중 상태를 가진다. 즉 부모 코루틴의 모든 코드가 실행됐지만 자식 코루틴이 아직 실행 중인 경우 부모 코루틴이 갖는 상태이다.
모든 코루틴 빌더 함수는 코루틴을 만들고 코루틴을 추상화한 Job 객체를 생성한다. Job 객체는 코루틴의 상태를 추적하고 제어하는 데 사용된다. Job 객체는 코루틴의 상태를 추적할 수 있는 상태 변수들을 외부로 공개하는데 Job 객체가 코루틴을 추상화한 객체이므로 외부로 노출되는 상태 변수들은 코루틴의 상태를 간접적으로만 나타낸다.
// Job.kt
public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
상태 변수들을 정의한 곳을 보면 알 수 있듯이 모두 Boolean 타입이다.
1. isActive : 코루틴이 활성화되어 있는지의 여부. 활성화되어 있으면 true, 아니면 false를 리턴한다.
2. isCompleted : 코루틴 실행이 완료됐는지 여부. 코루틴의 모든 코드가 실행 완료되거나 취소 완료되면 true를 리턴한다.
3. isCancelled : 코루틴이 취소 요청됐는지의 여부. 코루틴이 취소 요청되면 true를 리턴한다. 하지만 취소 요청된다고 바로 취소 완료되는 것은 아니므로, 이 변수가 true라고 해서 코루틴이 취소 완료되었다고 단정할 수 없다.
코루틴 상태에 따른 상태 변수들 값은 위와 같다.
사실 지금까지 작성한 내용은 코틀린 코루틴의 정석이라는 책의 내용을 바탕으로 작성한 것이다. 하지만 코루틴의 상태에 대한 주석 혹은 코드를 스스로 찾아보고 싶어서 찾아보았다.
@OptIn(InternalForInheritanceCoroutinesApi::class)
@Deprecated(level = DeprecationLevel.ERROR, message = "This is internal API and may be removed in the future releases")
public open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob {
final override val key: CoroutineContext.Key<*> get() = Job
/*
=== Internal states ===
name state class public state description
------ ------------ ------------ -----------
EMPTY_N EmptyNew : New no listeners
EMPTY_A EmptyActive : Active no listeners
SINGLE JobNode : Active a single listener
SINGLE+ JobNode : Active a single listener + NodeList added as its next
LIST_N InactiveNodeList : New a list of listeners (promoted once, does not got back to EmptyNew)
LIST_A NodeList : Active a list of listeners (promoted once, does not got back to JobNode/EmptyActive)
COMPLETING Finishing : Completing has a list of listeners (promoted once from LIST_*)
CANCELLING Finishing : Cancelling -- " --
FINAL_C Cancelled : Cancelled Cancelled (final state)
FINAL_R <any> : Completed produced some result
// ...
*/
private val _state = atomic<Any?>(if (active) EMPTY_ACTIVE else EMPTY_NEW)
// ...
internal val state: Any? get() {
_state.loop { state -> // helper loop on state (complete in-progress atomic operations)
if (state !is OpDescriptor) return state
state.perform(this)
}
}
// ...
}
JobSupport 클래스 정의된 곳을 보면 주석으로 state에 대해 작성된 곳이 있는데 여기서 public state라는 부분이 아마 외부로 노출한 코루틴의 상태이고 내부적으로는 EMPTY_ACTIVE
, EMPTY_NEW
같은 상태값을 사용하는 것 같다. 아마 책의 저자도 이 부분을 참고하지 않았을까.
그런데 코루틴 빌더 함수인 runBlocking, launch, async가 JobSupport 클래스와 얼핏 보면 관계 없는 것처럼 보인다. 하지만 코루틴 빌더 함수에서 생성되는 코루틴 객체는 모두 AbstractCoroutine
이라는 추상 클래스를 상속받은 클래스의 객체이다. 그리고 이 AbstractCoroutine
추상 클래스가 JobSupport
open class를 상속받는다.
그리고 JobSupport 클래스는 Job 인터페이스를 구현한다. Job 인터페이스에서 isActive
, isCompleted
, isCancelled
는 추상 프로퍼티로 선언되어 있었다. Job 인터페이스를 구현한 JobSupport 클래스에 이 프로퍼티가 override되어 재정의 되어 있다.
// JobSupport.kt
public override val isActive: Boolean get() {
val state = this.state
return state is Incomplete && state.isActive
}
public final override val isCompleted: Boolean get() = state !is Incomplete
public final override val isCancelled: Boolean get() {
val state = this.state
return state is CompletedExceptionally || (state is Finishing && state.isCancelling)
}