본문 바로가기
Kotlin/Coroutine

(Coroutine) 2. Light weight Thread, Job

by jaesungLeee 2022. 9. 12.

1. Light weight Thread == Coroutine

코루틴 공식 가이드를 살펴보면 코루틴을 경량화된 쓰레드(Light-weight Thread) 라고 표현하고 있다.

경량화된 쓰레드가 무엇일까?

 

앞서 살펴본 예시들의 출력 결과를 봐보자.

 

>> runBlocking: main @coroutine#1
>> Hello
>> launch: main @coroutine#2
>> World!

 

출력 결과를 통해 알 수 있듯이 코루틴은 쓰레드 상에서 실행된다.

위 결과는 main 쓰레드에서 수행된 2개의 코루틴에 대한 결과이다.

즉, 코루틴은 쓰레드 없이 단독적으로 수행되거나 다른 수단에 의해 실행될 수 없다는 것을 뜻한다.

그렇기 때문에, 여러 코루틴이 한 쓰레드에서 수행될 수 있지만 코루틴이 동시에 수행되는 것은 불가능하다.

 

하지만, 우리는 그동안 예시들을 통해 Suspension point가 있을 때 마다 다른 코루틴에게 쓰레드 점유를 양보하는 것을 확인했다.

이렇게 코루틴은 쓰레드 상에서 실행되지만 여러 코루틴들이 쓰레드 점유를 양보하면서 실행되기 때문에 쓰레드와 메모리 사용량이 줄어 많은 동시성 작업을 수행할 수 있다.

그렇기 때문에 코루틴을 경량화된 쓰레드라고 부를 수 있는 것이다.

 

동시성 작업을 위해 10,000개의 쓰레드를 만드는 것과 코루틴 10,000개를 만드는 것 중 어떤 것을 선택하는게 효율적일지 생각해보자.

 

2. Job

아래 코드를 살펴보자.

 

suspend fun doSomethingWithJob() = coroutineScope {
    launch {
        delay(1000L)
    }
}

fun main(): Unit = runBlocking {
    val result = doSomethingWithJob()
    println("result : ${result::class.simpleName}")
}

>> result : StandaloneCoroutine

 

launch 블록이 StandaloneCoroutine을 반환하고 있는 것을 확인할 수 있다.

그럼 이제 StandaloneCoroutine이 무엇인지 알아보자.

 

// Builders.common.kt
private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
    override fun handleJobException(exception: Throwable): Boolean {
        handleCoroutineException(context, exception)
        return true
    }
}

 

이전 포스팅에서 BlockingCoroutineAbstractCoroutine을 상속하고 있는 것을 확인했었다.

StandaloneCoroutineAbstractCoroutine을 상속하고 있다.

 

AbstractCoroutine은 이전 포스팅에서 살펴본 것처럼 CoroutineScope를 구현하기도 하지만 JobSupport라는 클래스도 상속하고 있다.

JobSupport 클래스의 주석에는 아래 글귀가 작성되어있다.

 

A concrete implementation of [Job]. It is optionally a child to a parent job.

 

그대로 해석해보면 [Job]의 구현체라는 의미이다.

즉, launch 코루틴 빌더가 반환하는 객체는 Job이라는 것이다.

우리는 앞으로 이 Job을 통해 코루틴을 제어할 것이다.

 

3. Job을 이용한 제어 (1) - join

아래 코드를 살펴보자.

 

suspend fun doOneTwoThreeWithJob() = coroutineScope {
    val job = launch {
        println("launch1: ${Thread.currentThread().name}")
        delay(1000L)    // suspension point
        println("3!")
    }
    job.join()    // suspension point

    launch {
        println("launch2: ${Thread.currentThread().name}")
        println("1!")
    }

    launch {
        println("launch3: ${Thread.currentThread().name}")
        delay(500L)    // suspension point
        println("2!")
    }
    println("4!")
}

fun main() = runBlocking {
    doOneTwoThreeWithJob()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}

>> launch1: main @coroutine#2
>> 3!
>> 4!
>> launch2: main @coroutine#3
>> 1!
>> launch3: main @coroutine#4
>> 2!
>> runBlocking: main @coroutine#1
>> 5!

 

launch 빌더는 Job 객체를 반환한다.

여기서 우리는 Job 객체에 join을 할 수 있게 되는데, join은 말그대로 기다리게하는 것이다. 

launchrunBlocking과는 다르게 쓰레드 점유를 양보한다고 설명하였지만, join을 통해 쓰레드 점유를 양보하지 않게 할 수 있다.

즉, 해당 Joblaunch 블록이 끝날 때 까지 기다릴 것이다.

결과적으로, 첫번째 launch 블록에 delay와 같은 Suspension point가 있지만 이 시점에 다른 코루틴이 동작하지 않게되고 1000ms 이후에 그대로 3!이 출력된다.

 

이전 포스팅을 통해 코루틴은 계층적 구조를 띄고 부모 코루틴이 실행된 다음 자식 코루틴이 실행되며, 자식 코루틴이 모두 완료될 때 까지 부모 코루틴은 기다린다고 설명했다.

4!는 왜 먼저 출력되지 않았을까?

 

Job에 대해 join을 하게 되면 이 지점이 Suspesion point이기 때문에 Job이 수행될 때 까지 기다린다.

그렇기 때문에 부모 코루틴에서 4!가 출력되는 것을 막는다.

join이 완료되고나면 부모 코루틴에서 4!가 출력될 것이고, 두번재 새번째 launch는 부모 코루틴의 작업이 완료된 후에 실행된다.

 

위 예시에서 중요한 점은 첫번째 launchdelay가 있어도 다른 코루틴에 양보하지 않는다는 점이다.

join을 하기 때문이다.

 

4. Job을 이용한 제어 (2) - cancel

이전 예시를 통해 우리는 Jobjoin함으로써 launch 블록이 다른 코루틴에게 쓰레드 점유를 양보하지 않도록 제어할 수 있었다.

이번에는 launch 코루틴 빌더가 반환하는 Job 객체를 통해 코루틴을 취소해보자.

 

suspend fun doOneTwoThreeWithJobCancel() = coroutineScope {
    val job1 = launch {
        println("launch1 : ${Thread.currentThread().name}")
        delay(1000L)
        println("3!")
    }

    val job2 = launch {
        println("launch2 : ${Thread.currentThread().name}")
        println("1!")
    }

    val job3 = launch {
        println("launch3 : ${Thread.currentThread().name}")
        delay(500L)
        println("2!")
    }

    delay(800L)
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}

fun main() = runBlocking {
    doOneTwoThreeWithJobCancel()
    println("runBlocking : ${Thread.currentThread().name}")
    println("5!")
}

>> launch1 : main @coroutine#2
>> launch2 : main @coroutine#3
>> 1!
>> launch3 : main @coroutine#4
>> 2!
>> 4!
>> runBlocking : main @coroutine#1
>> 5!

 

doOneTwoThreeWithJobCancel()은 800ms 이후에 job1, job2, job3 객체를 차례로 취소한다.

job2는 지연 없이 수행될 것이고, job3는 500ms의 지연만 있기 때문에 800ms의 지연이 진행되는 동안 다시 재개되어 2!까지 출력할 수 있다.

반면, job1의 경우, 1000ms의 지연이 있기 때문에 800ms의 지연이 끝난 후 다시 재개되지 못하고 cancel된다.

 

5. 취소 불가능한 Job (1) - cancel only

과연 모든 Job이 취소 가능할까?

정답은 "아니다" 이다.

 

아래 예시를 살펴보자.

 

suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 5) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }

    delay(200L)
    job1.cancel()
    println("doCount Done!")
}

fun main() = runBlocking {
    doCount()
}

>> 1
>> 2
>> doCount Done!
>> 3
>> 4
>> 5

 

코드를 잠시 살펴보면 우리는 200ms 이후에 job1cancel되도록 작성하였다.

하지만, 결과를 보면 job1은 취소되지 않았고 5까지 모두 출력되었다. 

왜 취소되지 않았을까?

 

launch 블록을 살펴보면 Dispatchers.Default라는 디스패쳐를 사용하고 있다.

이후 포스팅에서 디스패쳐에 대한 개념을 살펴볼 것이지만 Dispatchers.Default는 해당 코루틴 블록을 main 쓰레드가 아닌 별도의 쓰레드에서 수행시킨다고 우선 이해하면 된다.

그렇기 때문에, main 쓰레드에서 job1에 대해 취소하는 것이 불가능한 것이다.

 

6. 취소 불가능한 Job (2) - cancel & join

이번에는 작업 취소 여부에 상관없이 무조건 doCount Done! 이 마지막에 나오도록 해보자.

 

suspend fun doCount() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 5) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }

    delay(200L)
    job1.cancel()
    job1.join()
    println("doCount Done!")
}

fun main() = runBlocking {
    doCount()
}

>> 1
>> 2
>> 3
>> 4
>> 5
>> doCount Done!

 

일단 Job이 완료되기를 기다리기 위해 cancel 이후에 join을 사용한다.

그렇기 때문에 job1의 수행이 완료될 때 까지 기다리게 된다.

 

코루틴에서는 위 처럼 canceljoin을 한번에 할 수 있는 메서드를 제공한다.

앞으로는 cancelAndJoin을 사용하자.

 

// AS-IS
job1.cancel()
job1.join()

// TO-BE
job1.cancelAndJoin()

 

 

canceljoin을 통해 우리는 doCount Done!이 마지막에 나오도록 했다.

하지만, 여전히 우리가 원하던 job1cancel은 되지 않았다.

 

7. 취소 가능한 Job - isActive

위 예시들에서 우리가 Jobcancel을 못했던 결정적인 이유는 해당 JobDispatchers.Default를 통해 main 쓰레드가 아닌 다른 쓰레드에서 수행되었기 때문이다.

즉, main 쓰레드에서 아무리 cancel을 하려 해도 다른 쓰레드에서 수행되는 Jobcancel할 수 없었다.

 

하지만, 코루틴이 별도의 쓰레드에서 수행중이라고 해서 취소를 못하는 것 보다 아예 해당 코루틴의 모든 제어권을 가지고 있는 것이 더 안전하다.

이때 사용할 수 있는 속성은 바로 isActive이다.

 

아래 예시를 살펴보자.

 

suspend fun doCountRealCancel() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 5 && this.isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }

    delay(200L)
    job1.cancelAndJoin()
    println("doCount Done!")
}

fun main() = runBlocking {
    doCountRealCancel()
}

>> 1
>> 2
>> doCount Done!

 

코루틴의 활성화 여부에 따라 최종적으로 Job을 취소할 수 있게 되었다.

코루틴은 해당 코루틴 스코프 내에서 isActive를 통해 해당 코루틴이 활성화되어 있는지 확인할 수 있다.

우리는 해당 코루틴이 Active 상태 일때는 동작하고 Active 상태가 아닐 때 즉, 취소되었을 때는 출력되지 않게 한 것이다.

 

아래 예시를 통해 다시 확인해보자.

 

suspend fun doCountRealCancel() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 5) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                println(this)
                nextTime = currentTime + 100L
                i++
            }
        }
    }

    delay(200L)
    job1.cancelAndJoin()
    println("doCount Done!")
}

fun main() = runBlocking {
    doCountRealCancel()
}

>> 1
>> StandaloneCoroutine{Active}@3fcf5131
>> 2
>> StandaloneCoroutine{Active}@3fcf5131
>> 3
>> StandaloneCoroutine{Cancelling}@3fcf5131
>> 4
>> StandaloneCoroutine{Cancelling}@3fcf5131
>> 5
>> StandaloneCoroutine{Cancelling}@3fcf5131
>> doCount Done!

 

이번에는 isActive를 제외하고 코루틴을 출력하였다.

출력 결과를 보면 실제로 cancel이 호출된 200ms 이후부터는 코루틴의 상태가 Active에서 Cancelling으로 바뀐 것을 볼 수 있다.

즉, 실제로 코루틴의 상태는 cancel이 호출되면 Cancelling으로 변경되지만 앞서 설명했던 것 처럼 이 코루틴은 main 쓰레드가 아닌 다른 쓰레드에서 동작하기 때문에 취소되지 않았던 것이다.

 

8. 취소 가능한 Job - try, catch, finally

일반적으로 리소스를 사용하는 연산을 수행한다고 하면, Job을 취소하면서 리소스를 정상적으로 반환해야할 것이다.

안드로이드에서는 파일 입출력 연산, DB 연산, 네트워크 작업 등 컨텍스트가 필요한 연산이 예시가 될 것이다.

만약, launch에서 리소스를 할당 받아 위와 같은 연산을 수행했다면 정상적으로 리소스를 반환해주는 지점이 필요할 것이다.

이때, finally를 사용할 수 있다.

 

suspend 함수들은 JobCancellationException을 발생시키기 때문에 우리가 예외처리를 위해 보편적으로 사용하는 try, catch, finally로 대응할 수 있다.

 

아래 예시를 살펴보자.

 

suspend fun doOneTwoThreeTryFinally() = coroutineScope {
    val job1 = launch {
        try {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        } finally {
            println("job1 is finishing!")
        }
    }

    val job2 = launch {
        try {
            println("launch2: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        } finally {
            println("job2 is finishing!")
        }
    }

    val job3 = launch {
        try {
            println("launch3: ${Thread.currentThread().name}")
            delay(1000L)
            println("2!")
        } finally {
            println("job3 is finishing!")
        }
    }

    delay(800L)
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}

fun main() = runBlocking {
    doOneTwoThreeTryFinally()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}

>> launch1: main @coroutine#2
>> launch2: main @coroutine#3
>> launch3: main @coroutine#4
>> 4!
>> job1 is finishing!
>> job2 is finishing!
>> job3 is finishing!
>> runBlocking: main @coroutine#1
>> 5!

 

job1, job2, job3는 800ms 이후에 취소되도록 예약되었다.

cancel 되기 전 까지는 모든 jobtry문이 수행되고 있었지만 cancel이 되고난 이후부터는 finally가 수행된 것을 확인할 수 있다.

 

9. 취소하면 안되는 Job - withContext(NonCancellable)

어떤 코드는 중요한 작업이라 절대 취소하지 말아야할 경우가 있다.

예를 들어, 어떤 리소스는 무조건적으로 점유해야 하는 상황일 것이다.

이때는 명시적으로 Job을 취소 불가능하게 만들어야 한다.

취소 불가능한 Job을 만들기 위해 withContext(NonCancellable)을 사용할 수 있다.

 

아래 예시를 살펴보자.

 

suspend fun doOneTwoThreeWithContext() = coroutineScope {
    val job1 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        }
        delay(1000L)
        print("job1: end")
    }

    val job2 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        }
        delay(1000L)
        print("job2: end")
    }

    val job3 = launch {
        withContext(NonCancellable) {
            println("launch1: ${Thread.currentThread().name}")
            delay(1000L)
            println("2!")
        }
        delay(1000L)
        print("job3: end")
    }

    delay(800L)
    job1.cancel()
    job2.cancel()
    job3.cancel()
    println("4!")
}

fun main() = runBlocking {
    doOneTwoThreeWithContext()
    println("runBlocking: ${Thread.currentThread().name}")
    println("5!")
}

>> launch1: main @coroutine#2
>> launch1: main @coroutine#3
>> launch1: main @coroutine#4
>> 4!
>> 3!
>> 1!
>> 2!
>> runBlocking: main @coroutine#1
>> 5!

 

job1job2, job3는 모두 800ms 이후에 취소될 것을 예상할 수 있다.

그렇기 때문에 1!, 2!, 3!이 모두 출력되면 안된다.

하지만, withContext(NonCancellable)을 통해 우리는 해당 블록이 취소되면 안되는 블록이라고 명시하였기 때문에 800ms 이후에도 취소되지 않았다.

반면, withContext(NonCancellable)로 선언되어있지 않은 나머지 작업들은 정상적으로 취소됨을 볼 수 있다.

 

withContext(NonCancellable)finally와 결합하여 사용할수도 있다.

finally와 결합시켜 무조건 작업이 취소되지 않은 상태에서 리소스를 회수해야 하는 경우에는 안전하게 회수할 수 있다.

 

10. withTimeout

이전에 언급한 것 처럼 코루틴은 계층적 구조를 갖는다. 

부모 코루틴은 자식 코루틴의 작업이 완료될 때 까지 기다린다.

하지만, 우리는 상황에 따라 일정 시간이 지난 후에 강제로 Job을 종료해야 하는 경우가 있다.

API Timeout과 같은 상황일 것이다.

 

withTimeOut을 통해 일정 시간이 지난 후 Job을 강제로 취소할 수 있다.

 

아래 예시를 살펴보자.

 

suspend fun doCountWithTimeOut() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 5 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
}

fun main() = runBlocking {
    withTimeout(500L) {
        doCountWithTimeOut()
    }
}

>> 1
>> 2
>> 3
>> 4
>> Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 500 ms

 

500ms 이후 코루틴은 Timeout되어 TimeoutCancellationException이 발생하였다.

이렇게 Exception이 발생하게 되면 앞서 살펴본 것 처럼 try-catch로 예외처리를 하면 될것이다.

 

귀찮지 않을까?

 

11. withTimeoutOrNull

Timeout에 따른 TimeoutCancellationException을 처리하기 위해 반복되는 try-catch는 굉장히 번거로울 것이다.

이전에 cancel과 join을 한번에 할 수 있는 cancelAndJoin이 있듯이 Timeout시 발생하는 Exception을 한번에 처리할 수 있도록 하는 메서드가 있다.

withTimeoutOrNull을 사용하면 된다.

 

아래 예시를 살펴보자.

 

suspend fun doCountWithTimeOutOrNull() = coroutineScope {
    val job1 = launch(Dispatchers.Default) {
        var i = 1
        var nextTime = System.currentTimeMillis() + 100L

        while (i <= 10 && isActive) {
            val currentTime = System.currentTimeMillis()
            if (currentTime >= nextTime) {
                println(i)
                nextTime = currentTime + 100L
                i++
            }
        }
    }
}

fun main() = runBlocking {
    val result = withTimeoutOrNull(500L) {
        doCountWithTimeOutOrNull()
        true
    } ?: false
    
    println(result)
}

>> 1
>> 2
>> 3
>> 4
>> false

 

withTimeoutOrNullTimeoutCancellationException 발생 시 null을 리턴한다.

Kotlin에서 null 처리하는 방법은 여러가지이므로 적절하게 null 처리를 하면 된다.

위 예시에서는 엘비스 연산자를 사용해 null이 발생될 경우 false로 처리하고 나머지 정상적인 상황에서는 true로 처리하였다.

우리는 이 Boolean 값을 통해 타임아웃 발생 시 다음 로직을 처리할 수 있다. 

 

12. 부모가 있는 Job과 없는 Job

기본적으로, launch 블록을 통해 실행되는 코루틴은 부모가 존재하는 자식 코루틴이다.

해당 블록 안에서 다시 launch 블록을 열게 되면 마찬가지로 상위에 launch 블록이 부모가 될 것이다.

이러한 방식으로 코루틴은 계층적 구조를 띄게 된다는 것을 우리는 이미 알고있다.

 

하지만 명시적으로 Job을 만들면 어떻게 될까?

아래 예시를 살펴보자.

 

fun main() = runBlocking<Unit> {
    val job = launch { // 부모
        launch(Job()) { // 상위 launch와 부모-자식 관계가 성립되지 않게 됨
            println(coroutineContext[Job])
            println("launch 1: ${Thread.currentThread().name}")
            delay(1000L)
            println("3!")
        }

        launch { // 자식
            println(coroutineContext[Job])
            println("launch 2: ${Thread.currentThread().name}")
            delay(1000L)
            println("1!")
        }
    }

    delay(500L)
    job.cancelAndJoin()
    delay(1000L)
}

>> "coroutine#3":StandaloneCoroutine{Active}@368102c8
>> launch 1: main @coroutine#3
>> "coroutine#4":StandaloneCoroutine{Active}@6996db8
>> launch 2: main @coroutine#4
>> 3!

 

launch(Job())과 같이 명시적으로 JobCoroutineContext에 넣어주게 되면 해당 코루틴은 더 이상 자식 코루틴이 아니게 되고 상위 launch 블록을 통한 부모-자식 관게가 성립되지 않게 된다.

val job = launch {  }과 같이 job을 만들면 누가 부모 코루틴인지 컴파일러가 확인할 수 있지만, 임의로 Job을 만들게 되면 컴파일러는 알 수 없게 된다.

 

부모가 없는 Job은 코루틴의 구조적 동시성을 지키지 못한다.

구조적 동시성은 부모 코루틴은 자식 코루틴이 작업을 모두 완료할 때 까지 기다리고, 자식 중 그 누구라도 취소되면 부모 뿐만 아니라 형제 코루틴까지 모두 취소되는 구조적인 원칙이다.

즉, 부모-자식 관계는 더 이상 성립하지 않기 때문에 상위 launch 블록은 해당 코루틴이 작업을 모두 수행할 때 까지 기다리지 않을 것이다.

마찬가지로, 해당 코루틴에서 예외가 발생하여 취소되더라도 신경쓰지 않는다.

 

예시에서 cancelAndJoin을 하더라도 해당 코루틴이 취소되지 않고 3!을 출력한 이유이다.

 

fun main() = runBlocking {
    val elapsed = measureTimeMillis { 
        val job = launch { // 부모
            launch {  // 자식 1
                println("launch 1: ${Thread.currentThread().name}")
                delay(5000L)
            }
            
            launch {  // 자식 2
                println("launch 2: ${Thread.currentThread().name}")
                delay(10L)
            }
        }
        job.join()
    }
    
    println(elapsed)
}

>> launch 1: main @coroutine#3
>> launch 2: main @coroutine#4
>> 5044

 

해당 예시에서 자식 1은 5000ms을 대기하고, 자식 2는 10ms를 대기한다.

여기서 부모 코루틴은 자식의 작업이 모두 완료될 때 까지 기다릴 것이다.

 

아래 예시를 살펴보자.

 

fun main() = runBlocking {
    val elapsed = measureTimeMillis {
        val job = launch {  // 부모
            launch(Job()) {  // 자식 1?
                println("launch 1: ${Thread.currentThread().name}")
                delay(5000L)
            }

            launch {  // 자식 2
                println("launch 2: ${Thread.currentThread().name}")
                delay(10L)
            }
        }
        job.join()
    }

    println(elapsed)
}

>> launch 1: main @coroutine#3
>> launch 2: main @coroutine#4
>> 39

 

앞서 설명한 내용들을 이해할 수 있었다면 해당 예시의 출력 결과를 이해할 수 있을 것이다.

이전 예시에서 자식 1이라고 표시된 launch 블록을 변형시켜 Job을 통해 실행시키게 된다면 더 이상 상위 코루틴과의 부모-자식 관계가 성립되지 않기 때문에 부모 코루틴은 해당 작업을 기다리지 않을 것이다.

그렇기 때문에 자식 2의 작업이 모두 완료되면 부모 코루틴도 종료된다.

 

Reference

https://kotlinlang.org/docs/coroutines-overview.html

 

Coroutines | Kotlin

 

kotlinlang.org

 

'Kotlin > Coroutine' 카테고리의 다른 글

(Coroutine) 5. Scope  (0) 2022.11.01
(Coroutine) 4. CoroutineContext, Dispatcher  (0) 2022.09.14
(Coroutine) 3. async  (0) 2022.09.14
(Coroutine) 1. Coroutine Basic  (0) 2022.09.12
(Coroutine) 0. 개요 및 특징  (0) 2021.12.20