kotlin/기초

kotlin 코루틴 정리 - 1

blogger903 2024. 8. 25. 16:35
728x90

이번 포스트에서는 코드로 kotlin coroutine을 사용해보면서 특징을 파악합니다

크게 1,2로 나누어서 포스팅할 예정입니다

포스팅 내용은 코루틴을 익히면서 고도화 될 예정입니다

 

다루는 내용

  • runBlocking
  • 코루틴 취소
  • 코루틴 예외처리

runBlocking

kotlin에서는 다음과 같이 runBlocking으로 코루틴을 생성할 수 있습니다

runBlocking 내에서는 코루틴을 사용할 수 있게 됩니다

fun main () : Unit = runBlocking {
    printWithThread("START")
    launch { newRoutine() }
    printWithThread("END")
}

suspend fun newRoutine() {
    val num1 = 1
    val num2 = 2
    printWithThread("Sum: ${num1+ num2}}")
}

 

 

runBlocking 특징

runBlocking은 블로킹 코드 실행이 끝날때 까지 기다림
runBlocking은 기본적으로 호출된 스레드(일반적으로 메인 스레드)를 차단하고, 그 스레드에서 코루틴을 실행합니다. 특별한 CoroutineScope나 Dispatcher를 지정하지 않으면, runBlocking 내부의 코루틴은 다음과 같은 특성을 가집니다

  • 단일 스레드 실행: runBlocking 내부의 코루틴은 기본적으로 호출된 스레드에서 실행됩니다.
  • 순차적 실행: 코루틴들은 기본적으로 순차적으로 실행됩니다. 즉, 한 코루틴이 일시 중단되면 다음 코루틴이 실행될 수 있습니다.
  • Dispatchers.Default 사용 안 함: 특별히 지정하지 않으면 Dispatchers.Default와 같은 다른 디스패처를 사용하지 않습니다.

 

//출력결과:
//[2024-08-24T05:43:30.774408][main @coroutine#1] START
//[2024-08-24T05:43:32.786927][main @coroutine#2] Launch end
//[2024-08-24T05:43:32.789474][main] END
fun main() {
    runBlocking {
        printWithThread("START")
        launch {
            delay(2_000L)
            printWithThread("Launch end")
        }
    }
    printWithThread("END")
}

 

lazy execution

//출력결과:
//[2024-08-24T05:43:12.452365][main @coroutine#1] START
//[2024-08-24T05:43:13.461081][main @coroutine#2] lazy execution

fun main(): Unit = runBlocking {
    printWithThread("START")
    val job = launch(start = CoroutineStart.LAZY) {
        printWithThread("lazy execution")
    }

    delay(1_000L)
    job.start()
}

 

코루틴 비동기 실행

//출력결과:
//[2024-08-24T05:42:11.651632][main @coroutine#1] START
//[2024-08-24T05:42:12.662338][main @coroutine#2] job1
//[2024-08-24T05:42:12.662798][main @coroutine#3] job2
fun main(): Unit = runBlocking {
    printWithThread("START")
    val job1 = launch {
        delay(1_000L)
        printWithThread("job1")
    }

    val job2 = launch {
        delay(1000L)
        printWithThread("job2")
    }

}

 

코루틴 순차 블록킹 실행

//출력결과:
//[2024-08-24T05:45:15.705073][main @coroutine#1] START
//[2024-08-24T05:45:16.715395][main @coroutine#2] job1
//[2024-08-24T05:45:17.721882][main @coroutine#3] job2
//[2024-08-24T05:45:17.722563][main @coroutine#1] END
fun main(): Unit = runBlocking {
    printWithThread("START")
    val job1 = launch {
        delay(1_000L)
        printWithThread("job1")
    }

    job1.join()

    val job2 = launch {
        delay(1000L)
        printWithThread("job2")
    }

    job2.join()
    printWithThread("END")
}

 

코루틴 결과를 받을때는 async 사용

fun main(): Unit = runBlocking {
    val job = async {
        3 + 5
    }

    val result = job.await()
    printWithThread("Result: $result")
}

 

코루틴 내에서 다른 코루틴 호출

//출력결과:
//[2024-08-24T05:54:30.219526][main @coroutine#1] START
//[2024-08-24T05:54:30.229829][main @coroutine#2] asyncReturn1
//[2024-08-24T05:54:31.235452][main @coroutine#3] asyncReturn2
//[2024-08-24T05:54:32.238094][main @coroutine#1] Result: 2

fun main(): Unit = runBlocking {
    printWithThread("START")
    val job1 = async { asyncReturn1() }
    val job2 = async { asyncReturn2(job1.await()) }

    val result2 = job2.await()

    printWithThread("Result: ${result2}")
}

suspend fun asyncReturn1(): Int {
    printWithThread("asyncReturn1")
    delay(1_000L)
    return 1
}

suspend fun asyncReturn2(num: Int): Int {
    printWithThread("asyncReturn2")
    delay(1_000L)
    return num+1
}

 

코루틴 취소

기본적으로 코루틴을 취소하려면 delay, yield같은 kotlinx.coroutins 패키지의 suspend 함수를 사용해야합니다

//출력결과:
//[2024-08-24T05:42:54.086875][main @coroutine#1] START
//[2024-08-24T05:42:54.093769][main @coroutine#2] Count: 1
//[2024-08-24T05:42:54.599408][main @coroutine#2] Count: 2
fun main(): Unit = runBlocking {
    printWithThread("START")
    val job = launch {
        (1..5).forEach{
            printWithThread("Count: $it")
            delay(500L)
        }
    }

    delay(1_000L)
    job.cancel()
}

 

 

아래 코드 결과 맞춰보기

//출력결과
//[2024-08-25T23:25:15.822368][main @coroutine#2] Count: 1
//[2024-08-25T23:25:16.816044][main @coroutine#2] Count: 2
//[2024-08-25T23:25:17.816031][main @coroutine#2] Count: 3
// ... 5까지 출력되고 해당 코루틴은 종료됌

fun main(): Unit = runBlocking {

    val job1 = launch {

        var i = 1
        var nextPrintTime = System.currentTimeMillis()
        while (i <= 5) {
            if (nextPrintTime <= System.currentTimeMillis()) {
                printWithThread("Count: ${i++}")
                nextPrintTime += 1000L
            }
            if (!isActive) {
                throw CancellationException()
            }
        }
    }

    delay(100L)
    job1.cancel()
}

 

위코드는

launch에 CoroutinScope를 별도로 전달하지 않으면 같은 스레드에서 동작하니 해당 로직은 무한루프이기 때문에 delay코드가 동작안합니다

 

 

CoroutineContext를 분리해서 CancellationException을 던져서 취소시키기

 

// 출력결과
// [2024-08-25T23:39:43.078393][DefaultDispatcher-worker-1 @coroutine#2] Count: 1

fun main(): Unit = runBlocking {

    val job1 = launch(Dispatchers.Default) {

        var i = 1
        var nextPrintTime = System.currentTimeMillis()
        while (i <= 5) {
            if (nextPrintTime <= System.currentTimeMillis()) {
                printWithThread("Count: ${i++}")
                nextPrintTime += 1000L
            }
            if (!isActive) {
                throw CancellationException()
            }
        }
    }

    delay(100L)
    job1.cancel()
}

launch와 runBlocking 이 서로 다른 스레드에서 코루틴이 동작하기 때문에 launch와 delay가 서로 다른 스레드에서 실행되기 때문에

cancel이 바로 동작하여 isActive가 false라서 해당 분기에서 CancellationException이 발생합니다

 

delay()메서드도 내부적으로 CancellationException을 던지고 있었음

// 출력결과:
//[2024-08-25T23:44:43.006158][main @coroutine#1] 취소 시도
//[2024-08-25T23:44:43.900574][main @coroutine#2] delay로 취소되지 않음!


fun main(): Unit = runBlocking {
    val job1 = launch {
        try {
            delay(1_000L)
        } catch (e: CancellationException) {
//			do nothing!!
        }

        printWithThread("delay로 취소되지 않음!")
    }

    delay(100L)
    printWithThread("취소 시도")
}

 

코루틴 예외처리

//출력결과:
// Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.RuntimeException
//	at MainKt$main$1$job$1.invokeSuspend(Main.kt:276)...

fun main(): Unit = runBlocking {
    val job = CoroutineScope(Dispatchers.Default).launch {
        throw RuntimeException()
    }

    delay(1000L)
}

 

async 사용시 예외발생하지 않음

// 출력결과:
// Exception in thread "main" java.lang.RuntimeException
//	at MainKt$main$1$job$1.invokeSuspend(Main.kt:287)
//	at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:46)
//	at MainKt$main$1.invokeSuspend(Main.kt:291)

// 예외가 발생해도, 예외를 출력하지 않고
// 예외를 확인하려면 await()이 필요합니다
fun main(): Unit = runBlocking {
    val job = CoroutineScope(Dispatchers.Default).async {
        throw RuntimeException()
    }

    delay(1000L)
    job.await()
}

async 사용시 예외발생하지 않음은 아닙니다

단지 예외를 출력하지 않았을 뿐이고 async한 코루틴 job(DeferredCoroutine)에 대해서 await()를 해주면 예외가 출력됩니다

 

자식 코루틴의 예외는 부모 코루틴에 전달된다

//출력결과:
//Exception in thread "main" java.lang.RuntimeException
//  at MainKt$main$1$job$1.invokeSuspend(Main.kt:302)
//  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
//  at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
//  at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:280)
//  at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
//  at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
//  at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
//  at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
//  at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
//  at MainKt.main(Main.kt:300)
//  at MainKt.main(Main.kt)

// 자식 코루틴의 예외는 부모코루틴으로 전파된다
fun main(): Unit = runBlocking {
    val job = async {
        throw RuntimeException()
    }
}

 

만약 자식 코루틴의 예외를 부모 코루틴에 전파하고 싶지 않은경우에?

// SupervisorJob을 사용하면, 자식 코루틴의 예외를 부모 코루틴으로 전파하지 않음
fun main(): Unit = runBlocking {
    val job = async(SupervisorJob()) {
        throw RuntimeException()
    }
    delay(1000L)
}

 

CoroutineExceptionHandler 사용

 

// [2024-08-25T15:53:17.273475][DefaultDispatcher-worker-1 @coroutine#2] Caught java.lang.RuntimeException
//
//Process finished with exit code 0
//fun main(): Unit = runBlocking {
    val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        printWithThread("Caught $throwable")
    }

    val job = CoroutineScope(Dispatchers.Default).launch(exceptionHandler) {
        throw RuntimeException()
    }

    delay(1000L)
}

 

 

CoroutineExceptionHandler 주의할점

launch에만 적용 가능하며, 부모 코루틴이 있으면 동작하지 않음

// exceptionHandler
// [2024-08-25T15:54:05.694514][DefaultDispatcher-worker-1 @coroutine#2] Caught java.lang.RuntimeException
//Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.RuntimeException
//  at MainKt$main$1$job$1.invokeSuspend(Main.kt:351)
//  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
//  at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
//  at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
//  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
//  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
//  at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
//  Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [MainKt$main$1$invokeSuspend$$inlined$CoroutineExceptionHandler$1@540a1eaa, CoroutineId(2), "coroutine#2":StandaloneCoroutine{Cancelling}@7b649469, Dispatchers.Default]

// launch에만 적용 가능하며, 부모 코루틴이 있으면 동작하지 않음
fun main(): Unit = runBlocking {
    val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        printWithThread("Caught $throwable")
        throw throwable
    }

    val job = CoroutineScope(Dispatchers.Default).launch(exceptionHandler) {
        throw RuntimeException()
    }

    delay(1000L)
}

 

CancellationException인 경우 코루틴 취소로 간주하고, 부모 코루틴에 전파 하지 않으

// 발생한 예외가 CancelationException인 경우 부모 코루틴은 예외를 잡지 않음
// 그 외 다른 예외가 발생하는 경우 실패로 간주하고 부모 코루틴에 전파
fun main() = runBlocking {
    try {
        coroutineScope {
            // CancellationException을 발생시키는 자식 코루틴
            launch {
                println("자식 1: CancellationException 발생")
                throw CancellationException("자식 1 취소됨")
            }

            // 일반 예외를 발생시키는 자식 코루틴
            launch {
                println("자식 2: RuntimeException 발생")
                throw RuntimeException("자식 2 에러")
            }

            // 정상적으로 완료되는 자식 코루틴
            launch {
                println("자식 3: 정상 실행")
                delay(100)
                println("자식 3: 완료")
            }

            println("부모: 자식 코루틴 실행 중")
            delay(200) // 자식 코루틴들이 실행될 시간을 줍니다
        }
    } catch (e: Exception) {
        println("부모가 잡은 예외: ${e::class.simpleName} - ${e.message}")
    }

    println("메인: 완료")
}

 

Structured Concurrency

자식코루틴에서 예외가 발생했을때 부모 코루틴에 전달되고, 다른 자식 코루틴도 취소된다

//출력결과:
//부모: 시작
//자식 1: 시작
//자식 2: 시작
//자식 4: 시작
//자식 3: 시작
//자식 2: 작업 중...
//자식 4: 취소됨
//부모 Job: 예외로 인해 종료됨 - 자식 3에서 오류 발생!
//Exception in thread "main" java.lang.RuntimeException: 자식 3에서 오류 발생!
//  at MainKt$main$2$job$1$child3$1.invokeSuspend(Main.kt:493)
suspend fun main() = coroutineScope {
    val job = launch {
        println("부모: 시작")

        // 첫 번째 자식 코루틴
        val child1 = launch {
            println("자식 1: 시작")
            delay(500)
            println("자식 1: 작업 중...")
            delay(500)
            println("자식 1: 완료")
        }

        // 두 번째 자식 코루틴
        val child2 = launch {
            println("자식 2: 시작")
            delay(300)
            println("자식 2: 작업 중...")
            delay(300)
            println("자식 2: 완료")
        }

        // 세 번째 자식 코루틴 (예외 발생)
        val child3 = launch {
            println("자식 3: 시작")
            delay(400)
            throw RuntimeException("자식 3에서 오류 발생!")
        }

        // 네 번째 자식 코루틴 (취소 시연)
        val child4 = launch {
            try {
                println("자식 4: 시작")
                delay(2000) // 오래 실행되는 작업
                println("자식 4: 이 메시지는 출력되지 않을 것입니다")
            } finally {
                println("자식 4: 취소됨")
            }
        }

        // 자식 코루틴들이 완료될 때까지 기다림
        joinAll(child1, child2, child3, child4)
        println("부모: 모든 자식 작업 완료")
    }

    // 부모 Job이 완료될 때의 처리
    job.invokeOnCompletion { throwable ->
        when (throwable) {
            null -> println("부모 Job: 정상적으로 완료됨")
            is CancellationException -> println("부모 Job: 취소됨")
            else -> println("부모 Job: 예외로 인해 종료됨 - ${throwable.message}")
        }
    }

    delay(1000) // 일부 자식 코루틴이 실행을 마칠 시간을 줌

    // 부모 Job 취소 (모든 자식도 취소됨)
    job.cancel()

    // 부모 Job이 완전히 종료될 때까지 기다림
    job.join()

    println("메인: 모든 작업 완료")
}

 

단 예외로 CancellationException은 예외가 발생해도 부모 코루틴에 전파하지 않음

//출력결과:
//[2024-08-26T07:53:29.012582][DefaultDispatcher-worker-1 @coroutine#1] 부모: 시작
//[2024-08-26T07:53:29.01427][DefaultDispatcher-worker-2 @coroutine#2] 자식 1: 시작
//[2024-08-26T07:53:29.01443][DefaultDispatcher-worker-3 @coroutine#3] 자식 2: 시작
//[2024-08-26T07:53:29.01471][DefaultDispatcher-worker-4 @coroutine#4] 자식 3: 시작
//[2024-08-26T07:53:29.522315][DefaultDispatcher-worker-3 @coroutine#3] 자식 2: CancellationException 발생 - 자식 2 취소
//[2024-08-26T07:53:29.522691][DefaultDispatcher-worker-3 @coroutine#3] 자식 2: 정리 작업
//[2024-08-26T07:53:29.818435][DefaultDispatcher-worker-3 @coroutine#4] 자식 3: 예외 발생 - 자식 3에서 오류 발생!
//[2024-08-26T07:53:29.818591][DefaultDispatcher-worker-3 @coroutine#4] 자식 3: 정리 작업
//[2024-08-26T07:53:29.845433][DefaultDispatcher-worker-2 @coroutine#2] 자식 1: 정리 작업
//[2024-08-26T07:53:29.845529][DefaultDispatcher-worker-4 @coroutine#1] 부모: 자식에서 예외 잡음 - StandaloneCoroutine is cancelling
//[2024-08-26T07:53:29.845589][DefaultDispatcher-worker-4 @coroutine#1] 부모: 모든 자식 작업 완료
//[2024-08-26T07:53:29.84577][DefaultDispatcher-worker-2 @coroutine#2] 부모 Job: 예외로 인해 종료됨 - 자식 3에서 오류 발생!
//Exception in thread "main" java.lang.RuntimeException: 자식 3에서 오류 발생!
//  at MainKt$main$2$job$1$child3$1.invokeSuspend(Main.kt:576)
suspend fun main() = coroutineScope {
    val job = launch {
        printWithThread("부모: 시작")

        // 첫 번째 자식 코루틴 (정상 완료)
        val child1 = launch {
            try {
                printWithThread("자식 1: 시작")
                delay(1000)
                printWithThread("자식 1: 완료")
            } finally {
                printWithThread("자식 1: 정리 작업")
            }
        }

        // 두 번째 자식 코루틴 (CancellationException 발생)
        val child2 = launch {
            try {
                printWithThread("자식 2: 시작")
                delay(500)
                throw CancellationException("자식 2 취소")
            } catch (e: CancellationException) {
                printWithThread("자식 2: CancellationException 발생 - ${e.message}")
            } finally {
                printWithThread("자식 2: 정리 작업")
            }
        }

        // 세 번째 자식 코루틴 (일반 예외 발생)
        val child3 = launch {
            try {
                printWithThread("자식 3: 시작")
                delay(800)
                throw RuntimeException("자식 3에서 오류 발생!")
            } catch (e: Exception) {
                printWithThread("자식 3: 예외 발생 - ${e.message}")
                throw e  // 예외를 다시 던져서 부모로 전파
            } finally {
                printWithThread("자식 3: 정리 작업")
            }
        }

        try {
            joinAll(child1, child2, child3)
        } catch (e: Exception) {
            printWithThread("부모: 자식에서 예외 잡음 - ${e.message}")
        }

        printWithThread("부모: 모든 자식 작업 완료")
    }

    job.invokeOnCompletion { throwable ->
        when (throwable) {
            null -> printWithThread("부모 Job: 정상적으로 완료됨")
            is CancellationException -> printWithThread("부모 Job: 취소됨")
            else -> printWithThread("부모 Job: 예외로 인해 종료됨 - ${throwable.message}")
        }
    }

    job.join()
    printWithThread("메인: 모든 작업 완료")
}

 

'kotlin > 기초' 카테고리의 다른 글

kotlin 문법 기초 - 4  (0) 2024.07.23
kotlin 문법 기초 - 3  (0) 2024.07.17
kotlin 문법 기초 - 2  (1) 2024.07.14
kotlin 문법 기초-1  (0) 2024.07.10