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
반응형