ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Coroutine] 코루틴 학습 - 6 (Job and children awaiting)
    Java/Kotlin 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

    반응형

    댓글

Designed by Tistory.