코루틴 구조화의 중심에는 항상 Job 객체가 있다. 이렇게 구조화된 Job을 깨는 방법을 알아보며 구조화를 깨면 어떤 일이 발생하는지도 알아보자.
fun main() = runBlocking<Unit> {
launch(CoroutineName("Coroutine1")) { // Coroutine1 실행
launch(CoroutineName("Coroutine3")) { // Coroutine3 실행
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine4")) { // Coroutine4 실행
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
launch(CoroutineName("Coroutine2")) { // Coroutine2 실행
launch(CoroutineName("Coroutine5")) { // Coroutine5 실행
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
delay(1000L)
}
위 예시 코드를 조금씩 바꿔가면서 구조화 깨는 것을 진행해보려 한다.
예시 코드를 구조화한 모습은 위와 같다.
CoroutineScope 객체를 생성할 때 생성되는 Job 객체를 통해 새로운 루트 Job을 만들어서 구조화를 깰 수 있다.
fun main() = runBlocking<Unit> {
val newScope = CoroutineScope(Dispatchers.IO)
newScope.launch(CoroutineName("Coroutine1")) { // Coroutine1 실행
launch(CoroutineName("Coroutine3")) { // Coroutine3 실행
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine4")) { // Coroutine4 실행
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
newScope.launch(CoroutineName("Coroutine2")) { // Coroutine2 실행
launch(CoroutineName("Coroutine5")) { // Coroutine5 실행
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
delay(1000L) // 1초간 대기
}
// 실행결과
// [DefaultDispatcher-worker-8 @Coroutine4#5] 코루틴 실행
// [DefaultDispatcher-worker-1 @Coroutine5#6] 코루틴 실행
// [DefaultDispatcher-worker-3 @Coroutine3#4] 코루틴 실행
위 코드에서는 runBlocking 함수를 통해 루트 Job이 생성되지만, CoroutineScope(Dispatchers.IO)
을 호출하여 새로운 루트 Job이 생성된다.
그리고 구조화된 모습은 위와 같다. 만약 코드에서 맨 마지막에 delay를 호출하지 않았다면 실행결과는 아무것도 출력되지 않는다. runBlocking 코루틴은 자식 코루틴의 완료를 기다리는데, 현재 구조화가 깨져서 자식 코루틴을 가지고 있지 않다. 그래서 바로 메인 스레드 사용이 종료되어 프로세스가 종료된다. 이를 방지하기 위해서 delay를 호출하여 메인 스레드 사용이 종료되는 것을 방지한 것이다.
fun main() = runBlocking<Unit> {
val newRootJob = Job() // 루트 Job 생성
launch(CoroutineName("Coroutine1") + newRootJob) {
launch(CoroutineName("Coroutine3")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine4")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
launch(CoroutineName("Coroutine2") + newRootJob) {
launch(CoroutineName("Coroutine5")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
delay(1000L)
}
// 실행결과
// [main @Coroutine3#4] 코루틴 실행
// [main @Coroutine4#5] 코루틴 실행
// [main @Coroutine5#6] 코루틴 실행
이 코드에서는 Job()
을 통해 새로운 루트 Job인 newRootJob을 생성한다.
계층 구조는 위와 같다. delay를 호출한 이유도 위에서 "CorotuineScope 사용해 구조화 깨기" 파트에서 설명한 이유와 같다. 차이점은 "CoroutineScope 사용해 구조화 깨기"에서는 newScope가 루트 Job을 포함하는 방식이었지만, 여기서는 newRootJob 자체가 루트 Job이 된다는 점이다.
하지만 위와 같이 계층 구조가 끊어지면 독립적인 코루틴으로 분리되어 취소가 전파되지 않는다는 점을 유의하자. 취소 전파가 안되는 예시를 다른 예시로 살펴보자.
fun main() = runBlocking<Unit> {
val newRootJob = Job() // 새로운 루트 Job 생성
launch(CoroutineName("Coroutine1") + newRootJob) {
launch(CoroutineName("Coroutine3")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine4")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
launch(CoroutineName("Coroutine2") + newRootJob) {
launch(CoroutineName("Coroutine5") + Job()) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
delay(50L) // 모든 코루틴이 생성될 때까지 대기
newRootJob.cancel() // 새로운 루트 Job 취소
delay(1000L)
}
// 실행결과
// [main @Coroutine5#6] 코루틴 실행
일단 Coroutine5가 생성되기 전에 Coroutine2가 취소된다면 Coroutine5는 실행될 수 없기 때문에 모든 코루틴이 생성되도록 delay(50L)
을 추가하였다.
계층 구조를 보면 Job() 객체를 추가함으로 인해서 Coroutine5의 계층 구조가 끊어졌다. 그래서 취소 전파가 이루어지지 않고 Coroutine5가 정상적으로 실행된다.
fun main() = runBlocking<Unit> {
launch(CoroutineName("Coroutine1")) {
val coroutine1Job = this.coroutineContext[Job] // Coroutine1의 Job
val newJob = Job(parent = coroutine1Job)
launch(CoroutineName("Coroutine2") + newJob) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
}
// 실행결과
// [main @Coroutine2#3] 코루틴 실행
// (프로세스 종료 로그가 출력되지 않는다)
Job 객체를 생성하는 함수의 인자로 parent를 전달하여 부모를 설정할 수 있다. 이렇게 하면 구조화가 깨지지 않는다.
계층 구조를 그림으로 나타낸 모습은 위와 같다.
하지만 실행결과에서도 알 수 있듯이, 한 가지 문제가 존재한다. launch, async를 통해 생성한 Job 객체는 더 이상 실행할 코드가 없고, 모든 자식 코루틴들이 실행 완료되면 자동으로 실행 완료된다. 그러나 Job()
함수를 통해 객체를 생성하는 경우 자동으로 실행 완료되지 않는다는 문제가 존재한다.
그래서 newJob은 계속 '실행 중' 상태로 존재하고, 자식 코루틴이 실행 완료되지 않으면 부모 코루틴도 실행 완료되지 않으므로, 부모 코루틴인 Coroutine1과 그 부모의 runBlocking 코루틴은 '실행 완료 중' 상태에서 대기하게 된다.
이를 해결하기 위해서는 complete()
함수를 호출하여 newJob의 실행을 완료해야 한다.
fun main() = runBlocking<Unit> {
launch(CoroutineName("Coroutine1")) {
val coroutine1Job = this.coroutineContext[Job]
val newJob = Job(coroutine1Job)
launch(CoroutineName("Coroutine2") + newJob) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
newJob.complete() // 명시적으로 완료 호출
}
}
// 실행결과
// [main @Coroutine2#3] 코루틴 실행
// Process finished with exit code 0