비동기 스타일 함수
구조적인 동시성에서 벗어나기 위해 GlobalScope를 참조하는 async Coroutine Builder을 사용하여 doSomethingUsefulOne 및 doSomethingUsefulTwo을 실행하는 비동기 스타일의 함수를 정의할 수 있다. 이러한 함수들의 이름은 "...Async"를 접미사를 가지도록 하여, 함수들이 비동기 계산을 시작하기만 하고 결괏값을 얻기 위해 Deffered 값을 사용해야 한다는 것을 강조한다.
📖 GlobalScope는 사소하지 않은 역효과를 일으킬 수 있는 섬세하게 다뤄야 하는 API이다. 그 중 하나는 아래에서 설명될 것이며, 명시적으로 GlobalScope를 @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 함수들은 일시중단 함수가 아니라는 점에 주목하자. 이 함수들은 어디에서든지 사용될 수 있다. 하지만, 코드를 호출 할 때 이 함수들을 사용하면 이들의 동작은 언제나 비동기적(이곳에서는 동시성을 의미) 실행을 포함한다. 다음의 예는 그들이 Coroutine 바깥에서 어떻게 사용되는지에 대해 보여준다:
// 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")
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
📖 async 함수를 사용하는 프로그래밍 스타일은 다른 언어들에서 많이 사용되기 때문에 이곳에서 설명을 위해 제공된다. Kotlin Coroutines에서 이러한 스타일을 사용하는 것은 아래에서 설명되는 이유로 강하게 권장되지 않는다.
코드 상의 val one = somethingUsefulOneAsync() 행과 one.await() 표현식 사이에 약간의 논리 오류가 발생해, 프로그램이 예외를 발생시켜 프로그램에 의해 수행되던 작업이 중단되면 어떻게 되는지 생각해보자. 일반적으로 전역 오류 처리기는 이 예외를 잡아 개발자들을 위해 오류를 로깅하고 보고할 수 있지만, 그렇지 않으면 프로그램은 다른 작업을 계속할 수 있다. 하지만, 시작한 작업이 중단되었음에도 백그라운드에서 somethingUsefulOneAsync가 계속해서 실행중이다. 아래 세션에서 다루는 것처럼 이러한 문제는 구조적인 동시성을 적용한 경우에는 발생하지 않는다.*1
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. 구조적인 동시성을 적용하면 애러가 발생하면 오류가 발생한 Scope 내부의 코드들이 실행 취소되기 때문이다. 위 예에서는 runBlocking 이전에 오류가 발생하더라도 somethingUsefulOneAsync가 실행되는 GlobalScope에는 오류가 전파되기 않기 때문에 somethingUsefulOneAsync가 계속해서 실행되게 된다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Composing suspending functions - Async-style functions
원문 최종 수정 : 2022년 6월 27일
구조화된 동시성과 async
async를 사용한 동시 실행 예제를 사용하여 doSomethingUsefulOne과 doSomethingUsefulTwo를 동시에 실행하고 그들의 실행 결과를 합쳐서 반환하는 함수를 추출해보자. async Coroutine Builder가 CoroutineScope의 확장 함수로 정의되어 있기 때문에 이를 Scope내에 포함해야 하며, 이것이 coroutineScope 함수가 제공하는 기능이다.
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
이렇게 하면 concurrentSum 함수 내부에서 문제가 생겨서 예외가 발생 되었을 때, Scope 내부에서 실행된 모든 Coroutine들이 취소된다.
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
📌 전체 코드는 이곳에서 확인할 수 있습니다.
위 main 함수의 출력*1에서 보여지듯이, 두 작업들은 동시 실행된다.
The answer is 42
Completed in 1017 ms
취소는 언제나 Coroutines의 계층 구조를 통해 전파된다.
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()
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
자식들 중 하나(위에서는 two라는 변수로 명명됨)가 취소로 인해 실패하면 첫 async 함수*2와 await을 수행중인 부모*3가 모두 취소되는 방식에 유의하자
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. 전체 코드는 아래와 같고 doSomethingUsefulOne과 doSomethingUsefulTwo 함수들은 동시 실행되므로 1000ms 보다 약간 긴 시간이 걸릴 것임을 알 수 있다.
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")
}
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
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
}
*2. one 이라는 이름으로 명명된 async 블록을 의미한다.
*3. one과 two가 포함된 CoroutineScope을 의마한다. coroutineScope은 suspend fun이 포함되는 CoroutineScope을 가져오므로 fun main()에서 runBlocking으로 생성되는 CoroutineScope이 부모 Scope이 된다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Composing suspending functions - Structured concurrency with async
원문 최종 수정 : 2022년 6월 27일
목차로 돌아가기