(Kotlin Coroutines) Part 3 - Composing suspending functions

Kotlin Coroutines Part 3 - Composing suspending functions

이번엔 suspending function들의 구성 방법에 대해 알아보자.

Sequential by default

suspending function은 기본적으로 순차 실핼된다.

아래 예제를 보자.

1
2
3
4
5
6
7
8
9
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
}

위와 같이 두 개의 suspending function을 구성하고 호출해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}

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
}

출력 결과는 아래와 같다.

1
2
The answer is 42
Completed in 2008 ms

이를 통해 순차적으로 실행되고 있음을 알 수 있다.

Concurrent using async

이번엔 doSomethingUsefulOne(), doSomethingUsefulTwo() 두개의 메서드를 비동기호출로 동시에 수행해보자.

job 객체를 반환해주는 launch는 결과를 반환해주지않기때문에 비동기로 결과를 받을 수 있는 async를 활용해서 호출한다.

참고 kotlinx.coroutines#async

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}

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
}

출력 결과는 아래와 같다.

1
2
The answer is 42
Completed in 1018 ms

결과값은 똑같지만 비동기를 이용해 동시에 호출했기 때문에 수행시간이 절반으로 줄었음을 확인할 수 있다.

Lazily started async

async를 통한 코루틴의 호출도 바로 수행하지않고, 필요한 시점에 호출할 수 있다.

async 호출시 CoroutineStart.LAZY 속성을 넘겨주게 되면 start() 메서드 호출을 해야 코루틴을 실행하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
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")
}

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
}

출력 결과는 아래와 같다.

1
2
The answer is 42
Completed in 1016 ms

Async-style functions

GlobalScope.async를 이용해서 명시적인 비동기 메서드를 생성할 수 있다.

1
2
3
4
5
6
7
8
9
// The result type of somethingUsefulOneAsync is Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}

// The result type of somethingUsefulTwoAsync is Deferred<Int>
fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}

suspend 키워드가 존재하는 것이 아니기 때문에 suspending function은 아니지만, 어디에서든 호출이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import kotlinx.coroutines.*
import kotlin.system.*

// 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")
}

fun somethingUsefulOneAsync() = GlobalScope.async {
doSomethingUsefulOne()
}

fun somethingUsefulTwoAsync() = GlobalScope.async {
doSomethingUsefulTwo()
}

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
}

출력 결과는 아래와 같다.

1
2
The answer is 42
Completed in 1109 ms

의도한 대로 비동기를 통한 호출은 가능하지만, 이러한 방식은 코틀린에서 권고하지않는 방식이다.

Structured concurrency with async

위의 예제와 달리 아래와 같은 형식으로 비동기 호출을 하는 것이 권장되는 방식이다.

1
2
3
4
5
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}

다만 concurrentSum 메서드 안에서 예외가 발생하면 내부에서 수행된 모든 코루틴이 취소될 여지가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
}

출력 결과는 아래와 같다.

1
2
The answer is 42
Completed in 1016 ms

예외를 발생시키면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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()
}

출력 결과는 아래와 같다.

1
2
3
Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException