Java/Kotlin

[Coroutine] 코루틴 학습 - 6 (Job and children awaiting)

Icarus8050 2022. 5. 1. 00:18
반응형

Job과 children의 관계

  • 자식은 부모의 컨텍스트를 상속받는다.
  • 부모는 모든 자식들이 종료될 때까지 일시중단 된다.
  • 부모가 cancel되면, 자식 코루틴들도 cancel된다.
  • 자식이 destroy되면, 부모 또한 destroy된다.
fun main(): Unit = runBlocking(CoroutineName("main")) {
    val name = coroutineContext[CoroutineName]?.name
    println(name)   // main
    launch {
        delay(1000)
        val name = coroutineContext[CoroutineName]?.name
        println(name)   // main
    }
}

 

Job

  • Job은 생명주기에서 cancel 가능한 작업을 말한다.
  • Job의 라이프사이클은 상태로 나타낼 수 있다.

  • Active 상태에서 job은 필요한 작업을 실행한다.
    • job이 coroutine builder에 의해 생성되었을 때, 코루틴의 본문이 이 상태로 실행된다.
    • 이 상태에서 자식 코루틴을 시작할 수 있다.
    • 거의 모든 코루틴은 Active 상태에서 시작되는데, 지연 시작된 코루틴들은 'New' 상태에서 시작되고, 실행되기 위한 상태인 'Active' 상태로 변한다.
  • job이 인터럽트되지 않고 실행이 완료되면 'Completing' 상태로 변하고, 이러한 상태는 자식 코루틴들이 완료되길 기다린다.
  • 자식 코루틴들도 완료되면 job은 'Completed'로 되어 종료된다.
  • job은 'Active'나 'Completing' 중에 취소 또는 실패한다면 상태는 'Cancelling'으로 된다.
    • 'Cancelling' 상태에서는 데이터베이스 커넥션 종료나 리소스 해제와 같은 필요한 작업들을 수행할 수 있다.
    • 'Cancelling' 상태에서 필요한 모든 작업을 완료하고 나면 'Cancelled' 상태가 된다.
suspend fun main() = coroutineScope {
    val job = Job()
    println(job)    // JobImpl{Active}@ABC
    job.complete()
    println(job)    // JobImpl{Completed}@ABC

    val activeJob = launch { }
    println(activeJob)  // StandaloneCoroutine{Active}@DEF
    activeJob.join()
    println(activeJob)  // StandaloneCoroutine{Completed}@DEF

    val lazyJob = launch(start = CoroutineStart.LAZY) { }
    println(lazyJob)    // LazyStandaloneCoroutine{New}@GHI
    lazyJob.start()
    println(lazyJob)    // LazyStandaloneCoroutine{Active}@GHI
    lazyJob.join()
    println(lazyJob)    // LazyStandaloneCoroutine{Completed}@GHI
}
  • 위 코드 예시를 살펴보면, 지연실행되는 lazyJob은 자동으로 실행되지 않는다는 것을 확인할 수 있다.
  • 그 외 나머지는 생성되고나면 즉시 'Active' 상태가 되는 것을 확인할 수 있다.

 

부모 코루틴과 자식 코루틴의 참조

fun main(): Unit = runBlocking {
    val job: Job = launch {
        delay(3000)
    }

    val parentJob: Job = coroutineContext.job
    println(job == parentJob)   // false
    val parentChildren: Sequence<Job> = parentJob.children
    println(parentChildren.first() == job)  // true
}
  • 코루틴 빌더는 코루틴을 생성할 때, 부모 코루틴의 컨텍스트를 상속받아 생성되고, 부모와 자식은 서로 참조하고 있다.

 

suspend fun main(): Unit = coroutineScope {
    launch(Job()) {
        delay(2000)
        println("Will not be printed")
    }
}
// 아무것도 출력되지 않는다.
  • 만약 위와 같이 새로운 Job을 생성하여 부모 컨텍스트를 대체하게 되면 부모-자식 관계가 아니게 되어 Structured concurrency 메커니즘이 제대로 동작하지 않는다.
  • Job()을 통해서 부모 컨텍스트를 대체해 버렸기 때문에 바깥쪽 코루틴 스코프는 새로운 Job을 기다리지 않게되고, main() 함수가 실행되자마자 println()을 출력하지 않고 종료된다.
  • Job()을 통해서 부모 컨텍스트를 대체하지 않았다면, 부모 코루틴은 자식 코루틴이 종료될 때까지 기다리기 때문에 2초 후에 정상적으로 println()이 출력된다.

 

Children awaiting

  • job은 코루틴이 완료될 때까지 대기할 수 있다는 점은 큰 장점이다. 이는 join() 메서드를 통해 가능하다.
  • join() 메서드는 일시중단 함수이며, job이 final state(Completed 또는 Cancelled 상태)가 될 때까지 일시중단된다.
fun main(): Unit = runBlocking {
    val job1 = launch {
        delay(1000)
        println("Test1")
    }

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

    job1.join()
    job2.join()
    println("All tests are done")
}
// Test1
// Test2
// All tests are done
  • Job 인터페이스는 children 프로퍼티를 가지고 있는데, 이 프로퍼티는 모든 자식 코루틴을 참조하고 있다. 따라서 아래와 같이 모든 자식 코루틴들이 final state가 될 때까지 기다리도록 join() 메서드를 호출할 수 있다.
fun main(): Unit = runBlocking {
    val job1 = launch {
        delay(1000)
        println("Test1")
    }

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

    coroutineContext[Job]
        ?.children
        ?.forEach { it.join() }
    println("All tests are done")
}
// Test1
// Test2
// All tests are done

 

Job factory function

  • Job은 Job() 팩토리 함수를 이용하여 코루틴 없이도 생성할 수 있다.
  • 이러한 팩토리 함수를 이용해서 job을 생성하면 다른 코루틴과는 관계가 없는 컨텍스트가 생성되어 사용된다.
  • Job() 팩토리 함수로 생성해서 사용할 때 발생하는 일반적인 실수는 코루틴을 실행하고 나서 생성한 본문의 하위 job에 join() 함수를 호출하여 job이 완료될 때까지 대기하도록 하는 것이다. 이는 해당 코루틴이 종료되지 않고, 영원히 대기하게 된다.
  • 팩토리로 생성된 job은 하위 코루틴들이 모두 완료되었음에도 'Active' 상태이므로 종료되지 않는다. job이 다른 코루틴에 의해 사용될 준비를 하고 있기 때문이다.
suspend fun main(): Unit = coroutineScope {
    val job = Job()
    val job1 = launch(job) {	// 새로운 job으로 부모 컨텍스트를 대체한다.
        delay(1000)
        println("Text 1")
    }
    val job2 = launch(job) {	// 새로운 job으로 부모 컨텍스트를 대체한다.
        delay(3000)
        println("Text 2")
    }
    job.join()	// job은 종료되지 못하고 영원히 대기한다.
}
  • 더 나은 접근법은 children job을 join() 하는 방법이다. 아래의 코드는 자식 job이 종료되면 job도 함께 종료된다.
suspend fun main(): Unit = coroutineScope {
    val job = Job()
    val job1 = launch(job) {
        delay(1000)
        println("Text 1")
    }
    val job2 = launch(job) {
        delay(3000)
        println("Text 2")
    }
    job.children.forEach { it.join() }
}

 

CompletableJob

  • Job()은 겉으로 보기에는 Job 클래스의 생성자를 호출하는 것처럼 보이지만, 실제로는 Job 타입이 아니라 Job 인터페이스를 상속한 CompletableJob 타입을 리턴한다.
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)
  • CompletableJob 인터페이스는 Job에서 두 가지 메서드를 추가적으로 지원한다.

 complete()

  • job을 완료시킬 때 사용한다.
  • complete()가 호출되면 자식 코루틴이 완료될 때까지 실행상태를 유지하지만, 해당 job에서 새로운 코루틴은 시작되지 않는다.
  • job이 completed되었다면 true, 실패했다면 false를 반환한다.

completeExceptionally(exception: Throwable): Boolean

  • 주어진 exception과 함께 job을 예외적으로 complete 시킨다.
  • 모든 자식 코루틴은 즉시 canceled 된다.
fun main() = runBlocking {
    val job = Job()

    launch(job) {
        repeat(5) { num ->
            delay(200)
            println("Repeat num : $num")
        }
    }

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

    job.join()

    launch(job) {
        println("Will not be printed")	// job이 이미 complete되었기 때문에 새로운 자식 코루틴은 실행되지 않는다.
    }

    println("Done")
}
// Repest num : 0
// Repest num : 1
// Repest num : 2
// Repest num : 3
// Repest num : 4
// Done
fun main() = runBlocking {
    val job = Job()

    launch(job) {
        repeat(5) { num ->
            delay(200)
            println("Repeat num : $num")
        }
    }

    launch {
        delay(500)
        // 자식 코루틴들을 모두 cancel시키고 job을 complete 시킨다.
        job.completeExceptionally((Error("Something error")))
    }

    job.join()

    launch(job) {
        println("Will not be printed")
    }

    println("Done")
}
// Repeat num : 0
// Repeat num : 1
// Done

참고자료

https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837

반응형