1. Sequential by default
어딘가에 정의되어 원격 서비스 호출이나, 계산을 하는 유용한 suspending 함수 두개가 있다고 가정해봅시다.
우리가 유용하다고 가정했지만, 예제 목적에 따라서 실제로는 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
}
저 두 개의 함수를 순차적으로 호출한 뒤에 두 계산값의 합을 구하려면 어떻게 해야할까요?
실제로 우리는, 첫 번째 함수 결과를 가지고 두 번째 함수를 불러야 할지 결정하거나, 어떻게 호출할지를 결정합니다.
우리는 일반적인 순차적 호출을 사용할것입니다. 왜냐하면 코드는 코루틴 내부에 있고, 다른 일반적인 코드와 같이 sequential 이 default 이기 때문입니다.
아래 예제에서는 두 개의 suspending 함수들을 실행한 전체 시간을 측정하여 이를 증명하고 있습니다.
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 2012 ms
2. Concurrent using async
만약 doSomethingUsefulOne() 함수와 doSomethingUsefulTwo() 사이의 의존성이 없는 경우 concurrent 하게 동작하기를 원할 것입니다. 이 때, async 가 도움이 될 수 있습니다.
개념적으로 async는 launch입니다. async는 다른 모든 코루틴들과 concurrent하게 동작하는 별도의 코루틴을 시작합니다.
다른 점은 launch의 경우 Job 객체를 리턴하고, 다른 어떤 결과 값도 운반하지 않는 반면에, async는 Deferred ( light-weight non-blocking future )를 리턴하는 점입니다.
당신은 .await()을 사용하여 결과적으로 deferred의 결과값을 받을 수 있습니다. 그리고, 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 1022 ms
두 번째가 훨씬 빠릅니다. 왜냐하면 두 개의 코루틴이 concurrent하게 동작했기 때문입니다.
또, 코루틴의 concurrency는 항상 명시적이라는 점도 주목할만합니다.
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 1028 ms
만약 start 함수를 호출하는 것 대신에 println 내부에서 await을 호출했다면, 순차적으로 진행되는것에 주의해야 합니다.
await의 경우 코루틴 실행을 하고 완료될 때까지 기다리기 때문에, lazy에 대한 사용성을 고려하지 않았습니다.
3. Async-style functions
우리는 아래 두 함수 doSomethingUsefulOne() 과 doSomethingUsefulTwo()에 GlobalScope를 확용한 async 코루틴 빌더를 사용하여 비동적으로 호출할 수 있습니다.
코틀린에서는 비동기적인 연산으로만 사용하고, 결과값을 받기 위해서는 지연된 값을 받는 함수일 경우 이름 뒤에 ...Async를 붙여서 강조합니다.
GlobalScope의 경우 역효과가 날 수 있는 연약한 api입니다. 그것에 대해서는 아래 설명하겠습니다. 그래서@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 함수들이 아닌것을 주목해야 합니다. 어디에서든 호출될 수 있습니다. 그리고, 코드를 호출할 때 비동기적으로 (concurrent) 하게 실행됩니다.
// 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")
}
결과
The answer is 42
Completed in 1236 ms
이러한 스타일은 설명용으로만 제공되는데, 왜냐하면 다른 프로그래밍 언어에서는 인기있는 스타일이기 때문입니다.
하지만, 코틀린 코루틴에서는 아래와 같은 이유 때문에 권장되지 않습니다.
위 예제에서 val one = somethingUsefulOneAsync() 라인과 one.await() 표현 사이에 에러가 있어서, 예외가 날라가고 실행하던 작업이 중단되는것을 생각해볼 수 있습니다.
일반적으로 전역 에러 핸들러가 예외를 잡아서 log와 보고가 이루어 지겠지만, 프로그램을 다른 연산을 지속하고 있을 것입니다.
somethingUsefulOneAsync() 는 호출된 쪽이 중단되더라도 백그라운드에서 돌아갈 것입니다. (leak)
이러한 문제는 structured concurrency 에서 발생하지 않습니다.
4. Structured concurrency with async
위의 Concurrent using async 예제를 함수로 추출해서 concurrent하게 doSomethingUseFulOne()과 doSomethingUsefulTwo()를 수행하고 합산을 받아보겠습니다.
async 코루틴 빌더가 CoroutineScope의 확장으로 정의되어 있기 때문에 scope 내부로 둘 필요가 있고, coroutineScope에서 그러한 기능을 제공합니다.
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
이 방식대로라면, 만약 concurrentSum() 함수 내부 코드가 중간에 잘못되었고, 예외가 날라간다고 하더라도, 해당 scope 내부의 모든 launched 된 코루틴들은 취소될 것입니다.
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
결과
The answer is 42
Completed in 1042 ms
출력 결과를 보면 여전히 concurrent 하게 동작하는걸 확인할 수 있습니다.
취소는 코루틴 계층을 통해서 모든 경우에 전파됩니다.
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
second child 가 취소됨으로써 first child와 parent 역시 취소된 것을 볼 수 있습니다.
출처
https://kotlinlang.org/docs/composing-suspending-functions.html - composing-suspending-functions
'Kotlin' 카테고리의 다른 글
코루틴 취소와 타임아웃 (0) | 2022.06.01 |
---|---|
코루틴 basic (0) | 2022.06.01 |
코틀린 코루틴 개념 (0) | 2022.05.29 |
코틀린 1.4, 1.5 버전에서의 변경사항 (0) | 2021.08.15 |
(번역글) kotlin 1.5.20 릴리즈 노트 (0) | 2021.08.15 |