티스토리 뷰

728x90

 

코루틴 흐름 제어

  1. Job
  • 동시성 작업의 생명 주기를 표현하는 객체
  • 작업 상태를 추적하고, 작업을 취소할 수 있다.

  • 잡은 생성되자마자 시작되므로 Active 상태가 된다.
  • isActive, isCompleted, isCancelled 프로퍼티를 통해 상태를 추적할 수 있다.
  • Cancelled, Completed 상태일 때 isComplete의 프로퍼티 값이 모두 true다. isCancelled을 통해 두 상태를 구분한다.
  • launch(), async()에서 잡의 초기 상태를 지정할 수 있다.
    • CoroutineStart.DEFAULT: 잡을 즉시 시작한다. (default)
    • CoroutineStart.LAZY: 잡을 자동으로 시작하지 않는다. New 상태에서 시작을 기다린다.

 

 

  • start()
fun main() {
    runBlocking {
        val job = launch(start = CoroutineStart.LAZY) { // New
            println("Job started")
        }
    
        delay(100)
        job.start() // Active
    }
}
    • New 상태의 잡에 대해 start()를 호출하면 잡이 시작되면서 Active 상태가 된다.

 

 

  • children
fun main() {
    runBlocking {
        val job = coroutineContext[Job.Key]!!
         
        launch { println("task 1") }
        launch { println("task 2") }
         
        println("${job.children.count()} child running") // 2
    }
}
    • 잡이 다른 잡을 시작하면, 기존 잡이 새 잡의 부모가 된다.
    • children 프로퍼티를 통해 완료되지 않은 자식 잡들을 얻을 수 있다.
    • 코루틴이 본문을 끝내면 Completing 상태가 되어 자식들의 완료를 기다린다.
    • 모든 자식이 완료되면 상태가 Completing에서 Completed로 바뀐다.

 

 

  • join()
fun main() {
    runBlocking {
        val job = coroutineContext[Job.Key]!!
         
        val t1 = launch { println("task 1") }
        val t2 = launch { println("task 2") }
         
        t1.join()
        t2.join()
         
        println("${job.children.count()} child running") // 0
    }
}
 
    • 조인 대상 잡이 완료될 때까지 현재 코루틴을 일시 중단한다.
    • 조인 대상 잡이 New 상태라면 Active 상태가 되고 잡이 시작된다.

 

 

  • cancel()
    • cancel()이 호출되면, 코루틴의 취소를 확인하고 CancellationException을 발생시킨다.
suspend fun main() {
    val job = GlobalScope.launch(Dispatchers.Default) {
        var i = 1
        while (true) { println(i++) }
    }
     
    delay(100)
    job.cancel()
}
 
    • 위 코루틴은 취소되지 않고 계속 실행된다.

 

suspend fun main() {
    val job = GlobalScope.launch {
        var i = 1
        while (isActive) { println(i++) } // 취소됐는지 검사
    }
     
    delay(100)
    job.cancel()
}
 
    • isActive 프로퍼티를 통해 코루틴이 취소됐는지 검사한다.

 

 
fun main() = runBlocking {
    val parent = launch {
        GlobalScope.launch {
            println("child 1 started")
            delay(1000)
            println("child 1 finished")
        }
         
        launch {
            println("child 2 started")
            delay(1000)
            println("child 2 finished")
        }

        launch {
            println("child 3 started")
            delay(1000)
            println("child 2 finished")
        }
    }
     
    delay(500)
    parent.cancel()
    delay(1000)
    println("parent finished")
}

 

실행 결과
child 1 started
child 2 started
child 3 started
child 1 finished
parent finished
    • 부모 코루틴이 취소되면 자동으로 모든 자식의 실행을 취소한다.
    • GlobalScope에서 실행되는 코루틴은 부모가 취소되어도 영향을 받지 않는다.
    • 부모 코루틴은 GlobalScope 자식 코루틴이 끝날 때까지 기다려주지 않는다. 

 

 
fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("try $i")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {
                delay(1000)
                println("finally")
            }
        }
    }

    delay(1300L)
    job.cancelAndJoin()
}

 

실행 결과
try 0
try 1
try 2
finally
  • 이미 취소된 코루틴의 finally 블록 안에서 suspend 함수를 호출하면, CancellationException이 발생한다.
  • withContext로 NonCancellable context를 전달하여 처리할 수 있다.
  • NonCancellable은 isActive가 항상 true기 때문에 취소되지 않는다.

 

 

 

       2. 타임아웃

  • withTimeout()
fun main() {
    runBlocking {
        val asyncData = async { File("data.txt").readText() }
        try {
            val text = withTimeout(50) { asyncData.await() }
            println("Data loaded: $text")
        } catch (e: Exception) {
            println("Timeout exceeded")
        }
    }
}
    • withTimeout() 함수를 통해 코루틴 작업에 대한 타임아웃을 설정한다.
    • 50ms 안에 결과를 받아올 수 있으면 결과를 출력하고, 받아올 수 없다면 TimeoutCancellationException을 발생시킨다.

 

 

  • withTimeoutOrNull()
fun main() {
    runBlocking {
        val asyncData = async { File("data.txt").readText() }
        val text = withTimeoutOrNull(50) { asyncData.await() } ?: "Timeout"
         
        println("Data loaded: $text")
    }
}
    • 타임아웃이 발생하면 예외가 아닌 null을 반환한다.

 

 

 

       3. 디스패처

  • 특정 코루틴을 실행할 때 사용할 스레드를 제어한다.
  • context의 한 종류이기 때문에 코루틴 빌더 함수의 인자로 전달할 수 있다.
    • Dispatchers.Default: 스레드 풀 기반이고, 풀 크기는 사용 가능한 CPU 코어 수 또는 2가 된다. (default)
    • Dispatchers.IO: 스레드 풀 기반이고, I/O를 많이 사용하는 작업에 최적화되어 있다.
    • Dispatchers.Main: 메인 스레드를 사용한다. UI와 상호작용하는 작업을 실행한다.
    • Dispatchers.Unconfined: 현재 스레드에서 코루틴을 시작하지만, suspend 함수를 만나면 그 함수를 실행하는 스레드에서 수행된다.

 

  • Dispatchers.Unconfined
fun main() = runBlocking {
    launch(Dispatchers.Unconfined) {
        println(Thread.currentThread().name) // main
        delay(500)
        println(Thread.currentThread().name) // default executor
    }
}
    • delay 함수는 default executor 스레드에서 실행된다.
    • delay 함수를 호출한 후부터 코루틴의 스레드도 default executor로 변경된다.

 

fun main() {
    // 스레드 풀 크기가 5고 스레드 이름이 WorkThread인 디스패처 생성
    newFixedThreadPoolContext(5, "WorkThread").use { dispatcher ->
        runBlocking {
            for (i in 1..3) {
                launch(dispatcher) {
                    println(Thread.currentThread().name)
                    delay(100)
                }
            }
        }
    }
}
 
  • newFixedThreadPoolContext()를 통해 직접 만든 스레드 풀을 사용하는 디스패처를 만들 수 있다.
  • newSingleThreadPoolContext()를 통해 스레드 하나만 사용하는 디스패처를 만들 수 있다.

 

fun CoroutineScope.getName() = Thread.currentThread().name

fun main() {
    runBlocking {
        println("Parent: ${getName()}")                 // main
         
        launch {
            println("Child, inherited: ${getName()}")   // main
        }
         
        launch(Dispatchers.Default) {
            println("Child, explicit: ${getName()}")    // DefaultDispatcher-worker-1
        }
    }
}
 
  • 최상위 코루틴에서부터 디스패처가 자동으로 상속된다. 
  • 최상위 코루틴에서 디스패처를 지정하지 않으면 Dispatchers.Default가 된다. (runBlocking() 빌더는 현재 스레드 사용)

 

 

 

       4. 예외 처리

  • 1) 예외를 부모 코루틴에게 전달하는 방법  (launch)
    1. 자식 코루틴에서 예외 발생
    2. 부모 코루틴이 똑같은 오류로 취소
    3. 부모의 나머지 자식도 모두 취소
    4. 부모는 예외를 자신의 부모로 전달
    5. 최상위 코루틴에 도달할 때까지 과정 반복
    6. 예외가 CoroutineExceptionHandler에 의해 처리
fun main() {
    runBlocking {
        launch {
            throw Exception("Error in task 1")
            println("Task 1 completed")
        }
         
        launch {
            delay(1000)
            println("Task 2 completed")
        }
         
        println("Root")
    }
}
실행 결과
Root
Exception in thread "main" java.lang.Exception: Error in task 1
  • 첫 번째 자식 코루틴은 예외를 던진다.
  • 최상위 코루틴이 취소되고, 두 번째 자식 코루틴도 취소된다.

 

 

  • CoroutineExceptionHandler()
    • (CoroutineContext, Throwable) → Unit 타입인 람다를 인자로 받아 핸들러를 생성한다.
    • 핸들러도 context이기 때문에 코루틴 빌더의 context 인자로 넘길 수 있다.
suspend fun main() {
    val handler = CoroutineExceptionHandler{ _, exception ->
        println("Caught $exception")
    }
     
    GlobalScope.launch(handler) {
        launch {
            throw Exception("Error in task 1")
            println("Task 1 completed")
        }
         
        launch {
            delay(1000)
            println("Task 2 completed")
        }
         
        println("Root")
    }.join()
}
실행 결과
Root
Caught java.lang.Exception: Error in task 1
    • CoroutineExceptionHandler는 최상위 코루틴에서만 정의할 수 있다. → 예외를 부모로 전달하기 때문에
    • runBlocking은 handler를 지정해도, 디폴트 핸들러를 사용하게 된다.

 

 

  • 2) 발생한 예외를 저장해두기 (async)
    • 발생한 예외를 저장해두고, 그 계산 결과를 받아오기 위해 await() 함수를 호출했을 때 예외가 다시 발생한다.
 
fun main() {
    runBlocking {
        val deferred1 = async {
            throw Exception("Error in task 1")
            println("Task 1 completed")
        }
         
        val deferred2 = async {
            println("Task 2 completed")
        }
         
        deferred1.await() // 예외 발생
        deferred2.await()
        println("Root")
    }
}

 

실행 결과
Exception in thread "main" java.lang.Exception: Error in task 1
  • 첫 번째 자식 코루틴의 결과를 가져올 때 예외가 발생한다.
  • async와 같은 빌더들은 CoroutineExceptionHandler를 사용하지 않기 때문에 context에 핸들러를 설정해도 디폴트 핸들러가 사용된다.

 

 

fun main() {
    runBlocking {
        val deferred1 = async {
            throw Exception("Error in task 1")
            println("Task 1 completed")
        }
         
        val deferred2 = async {
            println("Task 2 completed")
        }
         
        try {
            deferred1.await() // 예외 발생하면서 부모가 중단됨
            deferred2.await()
        } catch (e: Exception) {
            println("Caught $e")
        }

        println("Root")
    }
}
실행 결과
Caught java.lang.Exception: Error in task 1
Root
Exception in thread "main" java.lang.Exception: Error in task 1
  • try-catch 블록으로 예외 처리를 시도해도 예외와 함께 중단된다.
  • 부모를 취소시키기 위해 자동으로 다시 예외를 던지기 때문이다.

 

 

  • supervisor job
fun main() {
    runBlocking {
        supervisorScope {
            val deferred1 = async {
                throw Exception("Error in task 1")
                println("Task 1 completed")
            }
         
            val deferred2 = async {
                println("Task 2 completed")
            }
         
            try {
                deferred1.await() // 예외 발생하면서 부모가 중단되지 않음
                deferred2.await()
            } catch (e: Exception) {
                println("Caught $e")
            }
        }
        println("Root")
    }
}
실행 결과
Task 2 completed
Caught java.lang.Exception: Error in task 1
Root
    • supervisor job이 있으면 부모는 취소되지 않고 자신의 자식만 취소된다.
    • supervisor job을 취소하면 자동으로 자신의 모든 자식을 취소한다.
    • 코루틴을 supervisor job으로 변환하려면 supervisorScope() 함수를 사용한다.

 

 

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }
    supervisorScope {
        val child = launch(handler) {
            throw AssertionError()
        }
        child.join()
        println("Scope finished")
    }
}
실행 결과
Caught java.lang.AssertionError
Scope finished
  • supervisor job에서는 자식 코루틴에서 핸들러를 설정할 수 있다.
 
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception")
    }
     
    val parent = GlobalScope.launch(handler) {
        val child = launch { throw IOException() }
        try {
            child.join()
        } catch (e: CancellationException) {
            println("Caught CancellationException")
        }
    }
    parent.join()
}
실행 결과
Caught CancellationException
Caught java.io.IOException
  • 취소 시 발생하는 CancellationException은 핸들러에게 무시된다.
  • CancellationException이 발생하면 슈퍼바이저 잡과 동일하게 부모는 취소되지 않고 자식만 취소된다.

 

 

 

 

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함