[Kotlin] Cancellation and timeouts

코루틴 공식 문서를 번역하고 내용을 조금 변경하거나 내용을 추가한 게시글입니다. 잘못된 번역이 있을 수 있습니다. 참고한 공식 문서 바로가기


Sequential by default

두 개의 다른 곳에서 정의된 suspending functions가 있다고 가정해보자. 또한 suspending functions는 원격 서비스를 호출하거나 계산을 하는 유용한 동작을 수행한다고 가정하자. (1초간 딜레이를 하는 부분이 유용한 기능을 수행한다고 가정)

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here, too
    return 29
}

만약 두 개의 함수가 순차적으로 실행되기를 원한다면 - 먼저 doSomethingUsefulOne을 실행하고 그 다음에 doSomethingUsefulTwo를 수행한다. 그리고 결과 값을 합친다? 실제로는, 첫 번째 함수의 결과를 사용하여 두 번째 함수를 호출해야 하는지 또는 호출 방법을 결정할 때 이 작업을 수행합니다.

일반 코드와 마찬가지로 코루틴의 코드는 기본적으로 순차적이기 때문에 일반 순차 호출을 사용합니다.

val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")
The answer is 42
Completed in 2017 ms    

Concurrent using async

만약 doSomethingUsefulOnedoSomethingUsefulTwo의 실행에 의존성이 없고 결과값을 더 빨리 얻고 싶다면, 두 개를 동시에 실행하면 됩니다. async를 사용하면 됩니다.

개념적으로 async는 launch와 같습니다. 별개의 코루틴(light-weight 하고 모든 코루틴과 동시에 작업하는 thread)을 시작합니다. launch는 Job을 반환하고 결과 값이 없는 반면 async는 Deferred (light-weight non-blocking fucture, 결과값을 나중에 제공함을 약속) 를 반환합니다. deferred value에 .await()를 사용하여 최종값을 얻을 수 있습니다. 하지만 Deferred 역시 Job이기 때문에 필요하다면 취소할 수 있습니다.

val time = measureTimeMillis {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
The answer is 42
Completed in 1017 ms   

두 개의 코루틴이 동시에 실행되기 때문에 2배 빠르다.

Lazily started async

async는 start 매개변수를 CoroutineStart.LAZY로 설정함으로써 lazy로 만들 수 있습니다. 이렇게하면 await에 의해 결과 값이 요구되거나 Job의 start 함수가 실행되었을 때만 코루틴이 시작됩니다.

val time = measureTimeMillis {
    val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
    val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
    // some computation
    one.start() // start the first one
    two.start() // start the second one
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
The answer is 42
Completed in 1017 ms   

이전 예제와는 달리, 2개의 코루틴이 있지만 실행되지 않습니다. 프로그래머가 start를 호출함으로써 실행을 시작할 수 있습니다. 먼저 one을 시작하고 그 다음 two를 시작하고 각각의 코루틴이 끝날 때까지 기다립니다.

만약 각각의 코루틴에 start를 호출하지 않고 println에서 await만 호출했다면, 이는 순차적인 행동을 하게 합니다. await은 코루틴 실행을 시작하고 끝날 때까지 기다리기 때문입니다. laziness의 use-case로 설계되지 않았습니다. async(start = CoroutineStart.LAZY)는 suspending functions를 포함한 computation 상황에서 표준 lazy 함수를 대체할 수 있습니다.

아래 예시를 보면 쉽게 이해할 수 있습니다.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        
        delay(500L)
        println("무언가 중요한 작업 ...")
        
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")    
}

suspend fun doSomethingUsefulOne(): Int {
    println("doSomethingUsefulOne Start")
    delay(1000L) // pretend we are doing something useful here
    println("doSomethingUsefulOne Finish")
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    println("doSomethingUsefulTwo Start")
    delay(1000L) // pretend we are doing something useful here, too
    println("doSomethingUsefulTwo Finish")
    return 29
}
doSomethingUsefulOne Start
doSomethingUsefulTwo Start
무언가 중요한 작업 ...
doSomethingUsefulOne Finish
doSomethingUsefulTwo Finish
The answer is 42
Completed in 1044 ms
import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
    	val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
        
        delay(500L)
        println("무언가 중요한 작업 ...")
        
        one.start() // start the first one
    	two.start() // start the second one
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")    
}

suspend fun doSomethingUsefulOne(): Int {
    println("doSomethingUsefulOne Start")
    delay(1000L) // pretend we are doing something useful here
    println("doSomethingUsefulOne Finish")
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    println("doSomethingUsefulTwo Start")
    delay(1000L) // pretend we are doing something useful here, too
    println("doSomethingUsefulTwo Finish")
    return 29
}
무언가 중요한 작업 ...
doSomethingUsefulOne Start
doSomethingUsefulTwo Start
doSomethingUsefulOne Finish
doSomethingUsefulTwo Finish
The answer is 42
Completed in 1526 ms

Async-style functions

GlobalScope 참조를 사용하여 비동기 코루틴 빌더를 사용하여 doSomethingUsfulOnedoSomethingUsfulTwo를 비동기적으로 호출하는 비동기식 함수를 정의하여 구조화된 동시성을 제거할 수 있습니다. 이러한 함수들을 “…Async” 처럼 접미사를 붙여 이름을 지어 비동기적인 계산을 시작하고 결과값을 얻기 위해서 deferred value를 사용해야함을 강조합니다.

GlobalScope는 사소한 일로도 역효과를 내는 섬세한 API입니다. (그 중 하나는 아래에서 설명합니다.) 따라서 GlopeScope를 사용하려면 @OptIn(DelicateCoroutinesApi::class)를 명시적으로 적어야합니다.

// The result type of somethingUsefulOneAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

// The result type of somethingUsefulTwoAsync is Deferred<Int>
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

xxxAsync 함수들은 suspending 함수가 아닙니다. 어디에서든지 사용할 수 있습니다. 그러나, 보통 asnchronous 실행임을 암시합니다. (여기서는 동시성을 의미)

코루틴 바깥에서 실행하는 예시입니다.

// note that we don't have `runBlocking` to the right of `main` in this example
fun main() {
    val time = measureTimeMillis {
        // we can initiate async actions outside of a coroutine
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()
        // but waiting for a result must involve either suspending or blocking.
        // here we use `runBlocking { ... }` to block the main thread while waiting for the result
        runBlocking {
            println("The answer is ${one.await() + two.await()}")
        }
    }
    println("Completed in $time ms")
}

비동기 함수가 있는 이 프로그래밍 스타일은 다른 프로그래밍 언어에서 인기 있는 스타일이기 때문에 설명용으로만 제공됩니다. 코틀린 코루틴과 함께 이 스타일을 사용하는 것은 아래에 설명된 이유로 매우 권장하지 않습니다.

만약 val one = somethingUsefulOneAsync()one.await() 사이에 에러가 있고 프로그램이 exception을 발생시키고, 프로그램에 의해 수행되고 있던 작업은 중단된다고 가정하자. 일반적으로, global error-handler가 exception을 catch하고 개발자들을 위해 로그를 남기고 보고한다. 그렇지않으면 프로그램은 다른 작업을 계속할 수 있다. 하지만, 시작된 작업이 중단되었음에도 불구하고 somethingUsefulOneAsync는 여전히 백그라운드에서 동작한다. 이런 문제는 아래 섹션에서 나오는 것처럼 structured concurrency에서는 발생하지 않는다.

Structured concurrency with async

Concurrent using async 예제를 가져와서 함수를 추출해보자. 추출하려는 함수는 doSomethingUsefulOnedoSomethingUsefulTwo를 동시에 수행하고 결과값들의 합을 리턴한다. async 코루틴 빌더가 CoroutineScope의 extension으로 정의되었기 때문에, async는 scope안에 있어야하고 scope는 coroutineScope 함수가 제공합니다.

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

이렇게하면, 만약 concurrentSum 함수 내부에서 뭔가 잘못되어 exception이 발생하면 해당 범위에 있는 모든 실행 중인 코루틴은 취소됩니다.

val time = measureTimeMillis {
    println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
The answer is 42
Completed in 1017 ms   

Cancellation은 항상 코루틴 계층으로 전파됩니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    try {
        failedConcurrentSum()
    } catch(e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum(): Int = coroutineScope {
    val one = async<Int> { 
        try {
            delay(Long.MAX_VALUE) // Emulates very long computation
            42
        } finally {
            println("First child was cancelled")
        }
    }
    val two = async<Int> { 
        println("Second child throws an exception")
        throw ArithmeticException()
    }
    one.await() + two.await()
}
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException

Categories:

Updated:

Leave a comment