부록 E. 코루틴과 Async/Await
CS/코틀린 인 액션

부록 E. 코루틴과 Async/Await

https://kotlinlang.org/docs/coroutines-guide.html

 

Coroutines guide | Kotlin

 

kotlinlang.org

https://speakerdeck.com/taehwandev/kotlin-coroutines

 

Kotlin coroutines

Jetbrains day in seoul 2018

speakerdeck.com

 

 

비동기는 알면 유용하게 쓰인다...

 

코루틴은 원래 코틀린 기능이 아니었지만 1.3버전에서 정식 채용된 플러그인.

# 수도코드
generator countdown(n) {
    while(n>0) {
        yield(n)
        n -= 1
    }
}

for i in countdown(10) {
    println(i)
}

yield를 실행해서 실행하던걸 멈추고 메인루틴으로 돌아가고 다시 실행하면 중단했던 곳 부터 다시 시작한다.

 

 

kotlinx.corutines.CoroutineScope.launch

사실 CorutineScope 안엔 launch 함수 하나밖에 없어서 CorutineScope는 CoroutineContext를 실행시키는 매개체에 불과하다.

import kotlinx.coroutines.*
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit

fun now() = ZonedDateTime.now().toLocalTime().truncatedTo(ChronoUnit.MILLIS)

fun log(msg:String) = println("${now()} [${Thread.currentThread().name}] $msg")

fun launchInGlobalScope() {
    GlobalScope.launch {
        log("launchInGlobalScope")
    }
}

fun main(args: Array<String>) {
    log("main start")
    launchInGlobalScope()
    log("launchInGlobalScope() called")
    Thread.sleep(5000L)
    log("main end")
}

/*
21:13:24.644 [main] main start
21:13:24.682 [main] launchInGlobalScope() called
21:13:24.684 [DefaultDispatcher-worker-1] launchInGlobalScope
21:13:29.687 [main] main end
*/

GlobalScope를 사용할 시의 주의점은 main 스레드가 끝나면 생성되었던 서브루틴도 같이 종료된다. 그래서 sleep이 없었으면 log도 안만들어보고 끝났을 것. 메인의 로그가 먼저 실행되는 이유는 launch를 실행할 때 메인스레드의 제어를 main()에 돌려주기 때문이라고 함.

이를 방지하려면 끝날때까지 기다리는 runblocking같은걸 써야 한다.

 

fun launchInGlobalScope() {
    runBlocking {
        GlobalScope.launch {
            log("launchInGlobalScope")
        }
    }
}

fun main(args: Array<String>) {
    log("main start")
    launchInGlobalScope()
    log("launchInGlobalScope() called")
    Thread.sleep(5000L)
    log("main end")
}

/*
21:18:15.482 [main] main start
21:18:15.552 [DefaultDispatcher-worker-1] launchInGlobalScope
21:18:15.553 [main] launchInGlobalScope() called
21:18:20.558 [main] main end
*/

 

그럼 그냥 서브루틴(일반함수) 쓰는것과 뭐가 다르냐? yield를 써서 스레드를 서로에게 넘겨주며 진행할 수 있다. 그래서 일반 함수에 yield 같은거 쓰면 컴파일 에러 뜬다. launchBlocking같은거랑 같이 쓰거나 비동기 함수에 써야함.

fun yieldExample() {
    runBlocking {
        launch {
            log("1")
            yield()
            log("3")
            yield()
            log("5")
        }
        log("after first launch")
        launch {
            log("2")
            delay(1000L)
            log("4")
            delay(1000L)
            log("6")
        }
        log("after second launch")
    }
}

fun main(args: Array<String>) {
    yieldExample()
}

/*
21:20:52.046 [main] after first launch
21:20:52.060 [main] after second launch
21:20:52.061 [main] 1
21:20:52.062 [main] 2
21:20:52.067 [main] 3
21:20:52.067 [main] 5
21:20:53.072 [main] 4
21:20:54.074 [main] 6
*/

yield로 넘겨줬지만 delay로 기다리느라 다시 제어권이 넘어가서 로그 출력이 숫자 차례대로 출력되지 않는다.

 

 

 

kotlinx.coroutines.CoroutineScope.async

async, await는 자세한 예시는 안나오지만 다른 언어와 비슷한 것 같다. 사실상 async와 위의 launch와 같은 일을 한다. 유일한 차이는 launc가 Job을 반환하는 반면 async는 Deffered를 반환한다는 점 뿐이다.

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
fun sumall() {
    runBlocking {
        val d1 = async { delay(1000L); 1 }
        log("after async(d1)")
        val d2 = async { delay(1000L); 2 }
        log("after async(d2)")
        val d3 = async { delay(1000L); 3 }
        log("after async(d3)")

        log("sum is ${d1.await() + d2.await() + d3.await()}")
        log("after await all & add")
    }
}


fun main(args: Array<String>) {
    sumall()
}

/*
21:39:57.052 [main] after async(d1)
21:39:57.079 [main] after async(d2)
21:39:57.080 [main] after async(d3)
21:39:58.103 [main] sum is 6
21:39:58.103 [main] after await all & add
*/

총 3초를 기다려야 할 것 같지만 1초만 기다림. 비동기적으로 실행되었기 때문. 프로젝트가 커지거나 실행시간이 긴 I/O같은거에 async/await를 쓰면 효과적이다.

 

launch, async 등은 모두 CoroutineScope의 확장 함수다. CoroutineScope 안에는 CoroutineContext를 실행하기 위한 도구일 뿐.

CoroutineContext는 실제로 코루틴이 실행 중인 여러 작업(Job 타입)과 디스패치를 저장하는 일종의 맵이라 할 수 있다.

fun log(msg:String) = println("${now()} [${Thread.currentThread().name}] $msg")

fun test() {
    runBlocking {
        launch {  // 부모 컨텍스트를 사용(이 경우 main())
            log("main runBlocking")
        }
        launch(Dispatchers.Unconfined) {  // 특정 스레드에 종속되지 않음 ? 메인 스레드 사용
            log("Unconfined runBlocking")
        }
        launch(Dispatchers.Default) {  // 기본 디스패처를 사용
            log("Default")
        }
        launch(newSingleThreadContext("MyOwnThread")) {  // 새 스레드를 사용
            log("newSingleThreadContext")
        }
    }
}


fun main(args: Array<String>) {
    test()
}

/*
21:49:56.633 [main] Unconfined runBlocking
21:49:56.646 [DefaultDispatcher-worker-1] Default
21:49:56.653 [MyOwnThread] newSingleThreadContext
21:49:56.653 [main] main runBlocking
*/

 

delay()나 yield()처럼 멈추는 다른 함수들

함수 설명
withContext 다른 컨텍스트로 코루틴을 전환한다.
withTimeout 코루틴이 정해진 시간 안에 실행되지 않으면 예외를 발생시키게 한다.
withTimeoutOrNull 코루틴이 정해진 시간 안에 실행되지 않으면 null을 결가로 돌려준다.
awaitAll 모든 작업의 성공을 기다린다. 작업 중 어느 하나가 예외로 실패하면 awaitAll도 그 예외로 실패한다.
joinAll 모든 작업이 끄탈 때까지 현재 작업을 일시 중단시킨다.