[coroutine] Composing Suspending Functions ( suspending 함수 만들기 ) |
Sequential by default
-
2개의 suspending function 이 있다고 가정해보자.
그리고 그 두 개의 함수가 remote service call 이나 계산을 하는 등의 의미있는 행동을 하며, 시간이 좀 걸린다고 해보자.
suspend fun doSomethingUsefulOne(): Int{
delay(1000L) // 의미있는 행동이라 가정
return 13
}
suspend fun doSomethingUsefulTwo(): Int{
delay(1000L) // 의미있는 행동이라 가정
return 29
}
위 두개의 함수가 순서대로 호출되길 원한다면 뭘해야 할까?
doSomethingUsefulOne 가 먼저 불리고 doSomethingUsefulTwo 가 불리고.. 그 다음 결과값을 더하고 싶다면?
첫번째 함수 결과가 두번째 함수를 수행하는데 반영한다면 이런 순서대로 호출이 꼭 필요할 것이다.
coroutine 안에서 기본적으로 순서대로 쓰인 코드는 기본적으로 순서대로 수행된다.
fun main() = runBlocking<Unit>{
val time = measureTimeMillis{
val one = doSomethingUserfulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int{
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int{
delay(1000L)
return 29
}
// Thw answer is 42
// Completed in 2017 ms
Concurrent using async
-
만약에 doSomethingUsefulOne 과 doSomethingUsefulTwo 가 상관관계가 없고, 동시 수행해서 더 빨리 답을 얻고싶다면 어떻게 해야 할까?
"async" 가 도움일 줄 것이다.
개념상으로 async 는 launch 와 비슷하다.
별개의 light-weight thread 인 coroutine 을 시작하고, 이 coroutine 은 다른 coroutine 들과 병렬적으로 작동한다.
차이가 있다면 launch 는 Job 을 return 하고 Job 은 결과값을 반영하지 않지만, async 는 light-weight non-blocking future인 Deferred 를 return 하고 이를 통해 결과값을 얻을 수 있다는 것이다.
.await() 함수를 통해 결과를 얻어올 수 있다.
그리고 Deffered 도 사실은 Job 이기 때문에 취소도 가능하다.
fun main() = runBlocking<Unit>{
val time = measureTimeMillis{
val one = async { doSomethingUserfulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int{
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int{
delay(1000L)
return 29
}
// Thw answer is 42
// Completed in 1017 ms
약 2배 빠르게 수행되었다. 왜냐면 2개의 coroutine 이 동시에 수행되었기 때문이다.
명심할 것은 coroutine 을 통한 동시성은 항상 명시적이라는 것이다.
Lazily started async
-
옵션으로 async 는 lazy 하게 시작될 수 있다. 이는 "start" param 에 CoroutineStart.LAZY 를 넘겨줌으로 가능하다.
이 모드에서는 await 함수가 불렸을 때 또는 Job 의 start 함수가 불렸을 때 coroutine 이 수행된다.
fun main() = runBlocking<Unit>{
val time = measureTimeMillis{
val one = async( start = CoroutineStart.LAZY ){ doSomethingUserfulOne() }
val two = async( start = CoroutineStart.LAZY ){ doSomethingUsefulTwo() }
one.start()
two.start()
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int{
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int{
delay(1000L)
return 29
}
// Thw answer is 42
// Completed in 1017 ms
이 예제에서 2개의 coroutine 은 정의되었지만 이전 예제처럼 바로 실행되지는 않는다.
start 를 불러서 실행시킬 수 있는 제어권을 프로그래머가 갖게 된다.
주의할 것은 우리가 start 함수들의 호출 없이 println 안에서 바로 await 를 호출했다면, 이는 순차적 실행을 했을 것이다.
왜냐하면 await 는 coroutine 을 수행하고 그것의 결과를 받아올 때까지 기다리기 때문이다.
async(start = CoroutineStart.LAZY 의 사용처는 계산 함수가 suspending function 을 가질 때 "lazy" 함수를 사용하는 것이 대체제이다.
Async-style functions.
-
우리는 async-style 함수를 정의하고 doSomethingUsefulOne 과 doSomethingUsefulTwo 를 async coroutine builder 를 GlobalScope 안에서 호출하도록 할 수 있다.
우리는 그런 함수를 "...Async" 라는 어미를 붙여서 async 계산이라는 것을 보여주고 있다.
그리고 그 함수 호출은 Deferred 를 return 할 것이라는 것을
fun somethingUsefulOneAsync() = GlobalScope.async{
doSomethingUsefulOne()
}
fun somethingUsefulTwoAsync() = GlobalScope.async{
doSomethingUsefulTwo()
}
주의할 것은 xxxAsync 함수들은 suspending function 이 아니라는 것이다.
이것은 어디서든 사용될 수 있으며, 단지 항상 async 라는 것을 보여주는 것 뿐이다.
fun main() {
val time = measureTimeMillis{
val one = somethingUsefulOneAsync()
val two = somethingUsefulTwoAsync()
runBlocking{
println("Thw answer is ${one.await() + two.await()")
}
}
println("Completed in $time ms")
}
fun somethingUsefulOneAsync() = GlobalScope.async{
doSomethingUsefulOne()
}
fun somethingUsefulTwoAsync() = GlobalScope.async{
doSomethingUsefulTwo()
}
suspend fun doSomethingUsefulOne(): Int{
delay(1000L)
return 13
}
suspend fun doSomethingUsefulTwo(): Int{
delay(1000L)
return 29
}
// Thw answer is 42
// Completed in 1017 ms
여기에 보여준 async function style 은 다른 프로그래밍 언어에서 인기있는 스타일이기에 오직 이렇게 할 수 있음을 보여주기 위함이다.
Kotlin coroutine 에서 이런 스타일로 사용하는 것은 "강력하게 비추" 이다.
val one = somethingUserfulOneAsync() 과 one.await() 사이에 어떤 로직이 들어가 있고, 그 로직이 이 함수 종료를 위해 exception 을 던진다면 무슨 일이 일어날까?
일반적으로 global error-handler 가 이 exception 을 잡고 log 를 찍은 후, 로직을 계속 수행한다.
그러나 이 함수 로직이 중단되었음에도 somethingUsefulOneAsync 는 계속해서 돈다.
이 문제는 아래의 structured concurrency 를 통해 피할 수 있다.
Structured concurrency with async
-
async coroutine builder 는 CoroutineScope 의 extension 으로 정의되어 있기 때문에, coroutineScope 함수를 통해서도 접근할 수 있다.
그리고 extract function 을 아래와 같이 해보자.
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
이 방법으로 concurrentSum 안에서 무언가가 잘못 되어 exception 을 던질 때, 모든 해당 scope 에서 launch 된 coroutine 은 모두 한번에 취소가 된다.
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
println("The answer is ${concurrentSum(){")
}
println("Computed 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)
return 13
}
suspend fun doSomethingUsefulTwo(): Int{
delay(1000L)
return 29
}
// Thw answer is 42
// Completed in 1017 ms
-
취소는 항상 coroutine hierarchy 를 통해 propagate(전파) 된다.
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("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
'프로그래밍 놀이터 > Kotlin, Coroutine' 카테고리의 다른 글
| [coroutine] Channels (5) | 2020.03.12 |
|---|---|
| [coroutine] Asyncronous Flow (0) | 2020.03.11 |
| [coroutine] Cancellation and Timeouts ( 취소와 타임아웃 ) (0) | 2020.03.09 |
| [coroutine] coroutineScope 의 동작 특성을 알아보자. (0) | 2020.03.08 |
| [coroutine] Coroutine Basics ( 코루틴 기초 ) (4) | 2020.03.07 |
댓글