-
[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
반응형'Java > Kotlin' 카테고리의 다른 글
[Coroutine] 코루틴 학습 - 8 (Exception handling) (0) 2022.05.05 [Coroutine] 코루틴 학습 - 7 (Cancellation) (0) 2022.05.03 [Coroutine] 코루틴 학습 - 5 (Coroutine context) (0) 2022.04.28 [Coroutine] 코루틴 학습 - 4 (Structured Concurrency) (0) 2022.04.27 [Coroutine] 코루틴 학습 - 3 (Coroutine Builders) (0) 2022.04.27