본문 바로가기
도서/코틀린 코루틴

2장 코루틴 라이브러리 - Job과 자식 코루틴 기다리기

by 안스 인민군 2024. 2. 3.

CoroutineContext에는 Job, CoroutineDispatcher, CoroutineName 이 있다고 이전 장에서 설명했다.

이 중 Job에 대해 더 자세히 알아보자.

Job

JobCoroutine Lifecycle를 관리하며, 부모-자식 관계의 특성을 가지고 있다.

더보기

부모 - 자식 설명

  • 자식은 부모로부터 Context를 상속받는다.
  • 부모는 모든 자식이 작업을 마칠 때 까지 기다린다.
  • 부모 코루틴이 취소되면 자식 코루틴도 취소된다.
  • 자식 코루틴에서 에러가 발생하면 부모 코루틴 또한 에러로 소멸된다.

Job Lifecycle

 

위의 이미지에 대해 알 수 있는 항목은

  • default 상태인 Active 상태는 Job을 실행한다.
  • 주어진 작업이 끝나면 Compleing 상태에서 자식이 모두 끝날 때 까지 기다린다.
    • 자식들이 모두 끝난다면 Completed 상태로 끝이 난다.
  • 만약 Job이 실행되는 도중이 취되거나 실패하면 Cancelling 상태로 가게 된다. 이 경우 어떻게 처리할 지 작업할 수 있다.
    • 작업이 끝난다면 Cancelled 상태로 끝이 난다.

해당 상태는 Job toString 함수로 알 수 있다.

fun main() {
    runBlocking {
        val job = Job()
        println(job)
        job.complete()
        println(job)
    }
}
// 결과
JobImpl{Active}@43814d18
JobImpl{Completed}@43814d18

상태 확인

State isActive isCompleted isCancelled
New (지연 시작될 때 시작 상태) false false false
Active (시작 상태 기본 값) true false false
Completing (일시적인 상태) true false false
Cancelling (일시적인 상태) false false true
Cancelled (최종 상태) false true true
Completed (최종 상태) false true false

Job 과 자식들간의 관계

모든 코루틴은 자신만의 Job을 생성하며, 인자 또는 부모 코루틴으로 온 잡은 새로운 Job의 부모로 사용된다.

fun main() {
    runBlocking {
        val name = CoroutineName("Some name")
        val job = Job()

        launch(name + job) {
            val childName = coroutineContext[CoroutineName]
            println(childName == name) //true
            val childJob = coroutineContext[Job] // 새로운 잡의 부모로 대체
            println(childJob == job) // false
            println(childJob == job.children.first()) // true
        }
    }
}
// 결과
true
false
true

위 설명처럼 CoroutineBuilder 에서 반환되는 Job으로 대체되어 childJob == job 가 false 이다.

자식 기다리기

Job의 첫번째 이점은 코루틴이 완료될 때까지 기다리는데 사용될 수 있다.

이를 위해 join 함수를 사용해야 한다.

joinJob이 최종 상태에 도달할 때 까지 기다리는 중단 함수 이다.

fun main(): Unit = runBlocking {
    val job1 = launch {
        delay(1000)
        println("Test1")
    }

    val job2 = launch {
        delay(2000)
        println("Test2")
    }

    job1.join()
    job2.join()
    println("All tests are done")
}
// (1초 후)
// Test1
// 1초 후
// (1초 후)
// All tests are done

아래는 join함수를 사용해, All tests are done 가 출력되기 전 job1, job2 의 CoroutineBuilder 가 실행되게 만들었다.

fun main(): Unit = runBlocking {
    launch {
        delay(1000)
        println("Test1")
    }

    launch {
        delay(2000)
        println("Test2")
    }

    val children = coroutineContext[Job]?.children
    val childrenNum = children?.count()
    println("Number of children: $childrenNum")
    children?.forEach { it.join() }
    println("All tests are done")
}
// 2
// (1초 후)
// Test1
// (1초 후)
// Test2
// All tests are done

자식 코루틴으로 반환되는 Job 객체를 사용하지 않아도 children 프로퍼티를 통해 접근할 수 있다.

Job 팩토리 함수

job 을 만들 때 val job = Job()형식으로 보통 만든다. 이 때 Job() 은 생성자가 아니라 사실 팩토리 함수 이다.

public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)

CompletableJob인터페이스는 두 가지 메서드를 추가하여 Job의 확장성을 높혔다.

public interface CompletableJob : Job {

    public fun complete(): Boolean

    public fun completeExceptionally(exception: Throwable): Boolean
}

complete() : Job을 완료하는데 사용한다.

  • 모든자식 코루틴은 실행이 완료될 때 까지 실행된 상태를 유지하지만 complete 를 호출한 Job에서는 새로운 코루틴이 시작될 수 없다.
fun main() {
    runBlocking {
        val job = Job()

        launch(job) {
            repeat(5) {
                delay(200L)
                println("$it")
            }
        }

        launch {
            delay(500)
            job.complete()
        }

        job.join()

        launch(job) { // 실행되지 않음
            println("Will not be printed")
        }

        println("Done")
    }
}
// 결과
0
1
2
3
4
Done

completeExceptionally : 인자로 받은 예외로 Job을 완료시킨다.

  • 모든 자식 코루틴은 예외를 래핑한 CancellationException으로 즉시 취소된다.