코루틴 중단 가능한 함수 조합 (Composing suspending functions)
이 섹션에서는 중단 가능한 함수를 조합하는 다양한 방법을 다룹니다.
기본적으로 순차적으로 실행
다른 곳에서 정의된 두 개의 중단 가능한 함수가 있다고 가정해보세요. 이 함수들은 원격 서비스 호출 또는 계산과 같은 유용한 작업을 수행한다고 가정합니다. 이 예제를 위해 각 함수가 1초 동안 지연됩니다.
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 유용한 작업을 하는 것처럼 가정
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 유용한 작업을 하는 것처럼 가정
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
async를 사용하여 병렬로 실행
만약 doSomethingUsefulOne과 doSomethingUsefulTwo의 호출 사이에 의존성이 없고 두 함수를 동시에 실행하여 더 빠른 답을 얻고 싶다면 어떻게 해야 할까요? 이때 async가 도움이 됩니다.
개념적으로 async는 launch와 마찬가지입니다. 별도의 코루틴을 시작하며 이 코루틴은 모든 다른 코루틴과 동시에 작동하는 가벼운 스레드입니다. 차이점은 launch는 Job을 반환하고 결과 값을 전달하지 않지만 async는 Deferred를 반환합니다. Deferred는 나중에 결과를 제공하는 약식의 비동기 미래를 나타냅니다. .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
두 배로 빠릅니다. 코루틴을 사용한 동시성은 항상 명시적입니다.
게으르게 시작된 비동기
async는 start 매개변수를 CoroutineStart.LAZY로 설정하여 선택적으로 게으를 수 있습니다. 이 모드에서는 결과가 await에 의해 필요할 때나 Job의 start 함수가 호출될 때만 코루틴을 시작합니다. 다음 예제를 실행하세요.
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
// 어떤 계산
one.start() // 첫 번째 코루틴 시작
two.start() // 두 번째 코루틴 시작
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
이는 다음과 같은 결과를 생성합니다:
The answer is 42
Completed in 1017 ms
따라서 여기에서 두 코루틴이 정의되지만 이전 예제처럼 실행되지는 않지만 실행을 언제 시작할지에 대한 제어권이 프로그래머에게 주어집니다. 먼저 하나를 시작한 다음 두 번째를 시작하고 개별 코루틴이 완료될 때까지 기다립니다.
개별 코루틴에 먼저 start를 호출하지 않고 println에서 await만 호출하면 이것은 순차적인 동작을 초래합니다. 왜냐하면 await는 코루틴 실행을 시작하고 완료를 기다리기 때문이며 이것은 게으른 사용 사례가 아닙니다. async(start = CoroutineStart.LAZY)의 사용 사례는 값의 계산에 중단 함수가 포함된 경우 표준 lazy 함수를 대체하는 것입니다.
Async 스타일 함수
이 async 함수를 사용한 프로그래밍 스타일은 다른 프로그래밍 언어에서 인기 있는 스타일이기 때문에 여기에서는 설명을 위해 제공됩니다. 그러나 Kotlin 코루틴과 함께 이 스타일을 사용하는 것은 아래에 설명된 이유로 강력하게 권장되지 않습니다.
우리는 async 코루틴 빌더를 사용하여 doSomethingUsefulOne 및 doSomethingUsefulTwo를 비동기적으로 호출하는 async-style 함수를 정의할 수 있습니다. 구조화된 동시성에서 제외하기 위해 GlobalScope 참조를 사용합니다. 이러한 함수를 결과 deferred 값을 얻기 위해 사용해야 하므로 이러한 함수의 이름에 "...Async" 접미사를 붙입니다.
GlobalScope는 비트 트릭한 API로, 비트 트릭한 방법 중 하나는 아래에서 설명하겠지만, DelicateCoroutinesApi::class를 사용하여 명시적으로 GlobalScope를 사용하도록 선택해야 합니다.
// somethingUsefulOneAsync의 결과 유형은 Deferred<Int>입니다.
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}
// somethingUsefulTwoAsync의 결과 유형은 Deferred<Int>입니다.
@OptIn(DelicateCoroutinesApi::class)
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}
이러한 xxxAsync 함수는 중단 함수가 아님에 유의하십시오. 어디서든 사용할 수 있습니다. 그러나 이러한 함수의 사용은 항상 호출 코드와 함께 비동기 (여기에서는 동시) 실행을 의미합니다.
다음 예제에서는 이러한 함수를 코루틴 외부에서 사용하는 방법을 보여줍니다:
// 이 예제에서는 `main` 오른쪽에 `runBlocking`이 없음에 유의하십시오.
fun main() {
val time = measureTimeMillis {
// 코루틴 외부에서 비동기 작업을 시작할 수 있습니다.
val one = somethingUsefulOneAsync()
val two = somethingUsefulTwoAsync()
// 그러나 결과를 기다리려면 중단 또는 차단이 필요합니다.
// 여기에서는 `runBlocking { ... }`을 사용하여 결과를 기다릴 동안 메인 스레드를 차단합니다.
runBlocking {
println("The answer is ${one.await() + two.await()}")
}
}
println("Completed in $time ms")
}
만약 `val one = somethingUsefulOneAsync()` 줄과 `one.await()` 표현식 사이에 코드에서 논리 오류가 발생하고 프로그램이 예외를 throw하며 수행 중인 작업이 중단되는 경우 어떻게 될지 생각해보세요. 일반적으로 전역 오류 처리기가 이 예외를 catch하여 개발자에게 로깅 및 오류 보고를 할 수 있지만 프로그램은 그 외에도 다른 작업을 계속할 수 있습니다. 그러나 여기에서는 작업을 시작한 비동기 함수인 somethingUsefulOneAsync가 여전히 백그라운드에서 실행되고 있습니다. 이러한 문제는 아래 섹션에서 보여주는 구조화된 동시성에서는 발생하지 않습니다.
구조화된 동시성을 이용한 async
Concurrent using async 예제를 살펴보고 doSomethingUsefulOne과 doSomethingUsefulTwo를 동시에 수행하고 그 결과의 합을 반환하는 함수를 추출해 보겠습니다. async 코루틴 빌더는 CoroutineScope의 확장으로 정의되므로 해당 스코프에 있어야 합니다. 이것이 coroutineScope 함수가 제공하는 역할입니다:
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
이렇게 하면 concurrentSum 함수 내부의 코드에서 문제가 발생하고 예외가 throw되면 해당 스코프에서 시작된 모든 코루틴이 취소됩니다.
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
다음은 주요 함수의 출력에서 확인할 수 있듯이 여전히 두 작업이 동시에 실행됩니다:
The answer is 42
Completed in 1017 ms
취소는 항상 코루틴 계층 구조를 통해 전파됩니다:
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) // 아주 오랜 계산을 흉내냅니다
42
} finally {
println("첫 번째 자식이 취소되었습니다")
}
}
val two = async<Int> {
println("두 번째 자식이 예외를 throw합니다")
throw ArithmeticException()
}
one.await() + two.await()
}
자식 중 하나(즉, two)에서 실패하면 첫 번째 async와 대기 중인 부모도 모두 취소됨을 확인하세요:
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException
원문
https://kotlinlang.org/docs/composing-suspending-functions.html
댓글