코루틴의 상태와 Job의 상태 변수

홍성덕·2024년 9월 24일
0

Coroutines

목록 보기
10/14

코루틴의 상태

코루틴은 그림과 같이 생성, 실행 중, 실행 완료 중, 실행 완료, 취소 중, 취소 완료 상태를 가질 수 있다.

  1. 생성(New) : 코루틴 빌더 함수를 통해 코루틴을 생성하면 코루틴은 기본적으로 생성 상태에 놓이며 자동으로 실행 중 상태로 넘어간다. launch와 async 빌더 함수에서는 start 인자로 CoroutineStart.Lazy를 전달하여 지연 코루틴을 만들면 코루틴이 생성 상태에 머물고 실행 중 상태로 자동으로 변경되지 않는다.
  2. 실행 중(Active) : 지연 코루틴이 아닌 코루틴을 생성하면 자동으로 시작하여 실행 중 상태로 바뀐다. 코루틴이 실행된 후 일시 중단이 될 때도 실행 중 상태이다.
  3. 실행 완료 중(Completing) : 다음 파트에서 좀더 자세히 설명하겠다.
  4. 실행 완료(Completed) : 코루틴의 모든 코드가 실행 완료된 경우 실행 완료 상태로 넘어간다.
  5. 취소 중(Cancelling) : cancel() 함수 등을 통해 코루틴에 취소가 요청되었을 때 취소 중 상태로 넘어간다. 이전 글에도 설명했지만 취소 요청을 했다고 해서 바로 취소되는 것은 아니다.
  6. 취소 완료(Cancelled) : 코루틴의 취소 확인 시점에 취소가 확인된 경우 취소 완료 상태가 된다. 이 상태에서 코루틴은 더 이상 실행되지 않는다.

코루틴의 실행 완료 중 상태

코루틴의 구조화는 큰 작업을 여러 작은 작업으로 나누는 방식으로 이뤄진다.
예를 들어, 여러 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 객체가 코루틴을 추상화한 객체이므로 외부로 노출되는 상태 변수들은 코루틴의 상태를 간접적으로만 나타낸다.

// 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)
    }

참고자료

profile
안드로이드 주니어 개발자

0개의 댓글

관련 채용 정보