(Kotlin Coroutines) 004. Suspend

Kotlin Coroutines - Suspend

앞선 포스팅의 예제에서 suspend 키워드를 사용하였다.

suspend 키워드가 붙은 함수는 언제든지 지연 혹은 재개될 수 있는 함수로 정의된다.

1. Sequential by default

suspend 함수를 순차적으로 실행해보자.

import kotlin.random.Random
import kotlin.system.*
import kotlinx.coroutines.*

suspend fun getLeft(): Int {
    delay(500L)
    return Random.nextInt(0, 100)
}

suspend fun getRight(): Int {
    delay(500L)
    return Random.nextInt(0, 100)
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val left = getLeft()
        val right = getRight()
        println("$left + $right = ${left + right}")
    }
    println(time)
}

출력 결과

1
2
60 + 44 = 104
1015

getLeft()getRight() 함수는 각각 0 부터 100 사이의 정수를 랜덤하게 반환한다.

이때 time 변수에 수행시간이 얼마나 걸렸는지 초기화한 뒤 출력한다.

이때 getLeft()getRight() 함수는 순차적으로 실행되므로 각 함수가 지연시키는 500L를 포함하여 1초를 근소하게 넘는 수행 시간을 가지게 될 것이다.

2. aysnc

예제를 다시 한 번 확인해보자.

import kotlin.random.Random
import kotlin.system.*
import kotlinx.coroutines.*

suspend fun getLeft(): Int {
    delay(500L)
    return Random.nextInt(0, 100)
}

suspend fun getRight(): Int {
    delay(500L)
    return Random.nextInt(0, 100)
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val left = getLeft()
        val right = getRight()
        println("$left + $right = ${left + right}")
    }
    println(time)
}

getLeft()getRight() 함수는 서로의 결과값에 대해 어떠한 영향도 끼치지않는다.

따라서 굳이 0.5초씩 기다려가며 순차적으로 수행할 필요가 없다는 뜻이다.

이때 사용하는 것이 async 키워드이다.

async 키워드를 사용하면 동시에 다른 블록을 수행할 수 있으며, launch와는 다르게 수행 결과를 await 키워드를 이용해 획득할 수 있다.

따라서 동시에 여러 블록을 실행하는 경우에 결과값이 필요하냐 아니냐에 따라 입맛대로 골라쓸 수 있다.

한 번 적용해보자.

import kotlin.random.Random
import kotlin.system.*
import kotlinx.coroutines.*

suspend fun getLeft(): Int {
    delay(500L)
    return Random.nextInt(0, 100)
}

suspend fun getRight(): Int {
    delay(500L)
    return Random.nextInt(0, 100)
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val left = async { getLeft() } // HERE
        val right = async { getRight() } // HERE
        println("${left.await()} + ${right.await()} = ${left.await() + right.await()}") // HERE
    }
    println(time)
}

출력 결과

1
2
74 + 37 = 111
576

async를 썼으니 결과값을 획득하기 위해 await를 사용하여 결과값을 출력하였다.

await 키워드는 async 블록의 수행이 끝나지 않았다면 suspend 되었다가, 수행이 완료되면 다시 재개되어 반환값을 받아온다.

출력된 수행시간을 보면 0.5초 + 0.5초 = 1초 +@의 수행시간이 아닌 0.5초대로 수행 완료했음을 알 수 있다.

3. Lazy async

async 키워드를 사용하는 시점부터 해당 코드 블록은 수행 준비를 한다.

허나 바로 실행할 것이 아니라면 이 대기또한 낭비가 될 것이다.

최적의 시간대에 코드 블록을 사용하기 위해 CoroutineStart.LAZY 속성과 start 함수를 이용해 블록을 수행할 타이밍을 결정해줄 수 있다.

아래 예제를 보자.

import kotlin.random.Random
import kotlin.system.*
import kotlinx.coroutines.*

suspend fun getLeft(): Int {
    delay(500L)
    return Random.nextInt(0, 100)
}

suspend fun getRight(): Int {
    delay(500L)
    return Random.nextInt(0, 100)
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val left = async(start = CoroutineStart.LAZY) { getLeft() } // HERE
        val right = async(start = CoroutineStart.LAZY) { getRight() } // HERE
        left.start()
        right.start()
        println("${left.await()} + ${right.await()} = ${left.await() + right.await()}")
    }
    println(time)
}

출력 결과

1
2
12 + 84 = 96
527

4. async를 사용한 구조화된 동시성(Structured concurrency using async)

코드를 작성하다보면 예기치않게 예외가 발생할수 있다.

코루틴은 계층적 구조로 되어있기 때문에 특정 코루틴 내에서 예외가 발생하는 경우, 부모 코루틴과 자식 코루틴에 모두 전파되어 코루틴 스코프가 취소된다.

이번에도 예제를 통해 알아보자.

import kotlin.random.Random
import kotlin.system.*
import kotlinx.coroutines.*

suspend fun getLeft(): Int {
    delay(500L)
    return Random.nextInt(0, 100)
}

suspend fun getRight(): Int {
    delay(500L)
    throw IllegalStateException()
}

suspend fun doSomething() = coroutineScope {
    val left = async { getLeft() }
    val right = async { getRight() }
    try {
        println("${left.await()} + ${right.await()} = ${left.await() + right.await()}")
    } finally {
        println("doSomething is cancealed")
    }
}

fun main() = runBlocking {
    try {
        doSomething()
    } catch(e: IllegalStateException) {
        println("doSomething is failed : $e")
    }
}

출력 결과

1
2
doSomething is cancealed
doSomething is failed : java.lang.IllegalStateException

getRight() 함수에서 예외가 발생하였지만, 부모 코루틴에 해당하는 doSomething()의 스코프도 종료되었음을 확인할 수 있다.