[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 |
댓글