Kotlin 공부

kotlinlang.org 내가 보려고 정리 - Coroutines basics

3190024 2023. 8. 21. 13:54

Coroutines basics

Your first coroutine

코루틴은 유예할 수 있는 계산의 인스턴스..

나머지 코드와 동시에 작동하는 코드 블록을 실행해야 한다는 점에서 스레드랑 비슷해 보인다.

하지만 코루틴은 특정한 스레드에 바인딩되지 않음.

하나의 스레드에서 실행을 유예하고 다른 스레드에서 재개함.

 

가벼운 스레드로 생각할 수 있지만, 스레드와는 실사용에서 매우 다르게 만드는 중요한 차이점들이 있다.

 

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello") // main coroutine continues while a previous one is delayed
}
/*
Hello
World!
*/

launch: 코루틴 빌더. 새로운 코루틴을 계속 독립적으로 작업하는 나머지 코드와 동시에 시작한다.

delay: 특별한 suspending function. 특정 시간동안 코루틴을 유예함. 코루틴을 미루는 것은 기본 스레드를 막지 않지만, 다른 코루틴들이 실행되고 그 코루틴들 코드에 대한 기본 스레드를 사용할 수 있게 한다.

runBlocking: main()의 코루틴이 아닌 영역과 runBlocking{}안의 코루틴이 있는 코드를 연결하는 코루틴 빌더. 새로운 코루틴을 실행하고 코루틴의 완료까지 현재 스레드를 블록함. 코루틴에서 이 함수를 사용하면 안됨. runBlocking이 어플리케이션의 아주 탑레벨에서 사용되고 실제 코드 내부에서는 아주 드물게 쓰이는데, 스레드가 값비싼 자원이고 스레드를 막는 것은 비효율적이고 종종 바람직하지 않기 때문.

 

위의 코드에서 runBlocking을 빼먹으면 launch를 호출할 때 에러 발생.

launch는 Coroutine Scope에서만 선언되기 때문.

 

Structured concurrency

새로운 코루틴들은 코루틴의 라이프 타임을 정하는 특정 CoroutineScope에서만 시작될 수 있다.

위의 예시에서 runBlocking은 해당 범위를 설정하므로 위의 예제가 World! 가 1초의 지연 후에 출력되고 종료됨을 보여준다.

 

코루틴들을 잃어버리지도 않고 새지도 않게 보장한다.

바깥 범위는 자식 코루틴들이 완료될 때까지 완료될 수 없다.(>> 자식 코루틴들을 기다려준다?)

코드안의 그 어떤 에러든 적절하게 보고하고 놓치지 않는 것을 보장한다.

 

Extract functions refactoring

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { doWorld() } // doWorld() 호출
    // launch { justCall() } // 오류 발생. suspend 함수 doWorld()는 코루틴 내부 또는 또다른 suspend 함수에서만 호출될 수 있다.
    println("Hello")
}

/*
fun justCall(){
    doWorld()
}
*/

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L) // 1초 기다리고
    println("World!") // 출력
}
/*
Hello
World!
*/

suspend 수식어를 단 Suspending function들은 보통 함수 처럼 코루틴 내부에 쓰일 수 있지만, 추가적인 특징은 차례로 코루틴의 실행을 미루는 (delay()와 같은..) 다른 suspend 함수들을 사용할 수 있다는 것이다.

(suspend 함수는 코루틴 내부 또는 또다른 suspend 함수에서만 호출될 수 있다.)

 

Scope Builder

다른 빌더들이 제공하는 코루틴 스코프에 더해서, coroutineScope 빌더를 사용하여 범위를 선언하는 것이 가능하다.

coroutineScope 빌더는 코루틴 스코프를 만들고, 시작된 자식들이 완료되기 전까지는 완료되지 않는다.

 

runBlocking coroutineScope
둘 다 body와 children이 완료되기를 기다린다는 점에서 비슷해보임
기다리기 위해 현재 스레드를 차단함.
regular function
다른 용도를 위해 기본 스레드를 유예했다가 다시 시작함.
 suspending function

모든 suspending 함수에서 coroutineScope를 사용할 수 있다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    doWorld()
}

suspend fun doWorld() = coroutineScope {  // this: CoroutineScope
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello")
}
/*
Hello
World!
*/

 

Scope builder and concurrency

coroutineScope 빌더는 여러 개의 동시 작ehd을 위해 모든 suspending 함수 내부에 쓸 수 있다.

import kotlinx.coroutines.*

// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking { // 순서대로 실행
    doWorld()
    println("Done")
}

// 두 섹션을 동시에 실행
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
    launch {
        delay(2000L)// 2초 기다리기
        println("World 2")
    }
    launch {
        delay(1000L) // 1초 기다리기
        println("World 1")
    }
    println("Hello")
}
/*
Hello
World 1
World 2
Done
*/

launch{} 블록들은 동시에 실행됨.

doWorld() 함수의 coroutineScope는 두 launch{} 블록들이 완료된 후에 완료된다.

따라서 doWorld가 반환되고나서야 "Done"이 출력될 수 있다.

 

An explicit job

launch라는 coroutine builder는 시작된 코루틴을 다루고 코루틴의 완료까지 명시적으로 기다리는 데 쓰이는 Job 객체를 반환한다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch { // 새로운 코루틴을 시작하고 Job 객체를 반환하여 참조를 유지
        delay(1000L) // 1초 기다리고
        println("World!") // 출력
    }
    println("Hello")
    job.join() // 자식 코루틴이 완료되기를 기다림.
    println("Done")     
}
/*
Hello
World!
Done
*/

 

Coroutines are light-weight

코루틴은 JVM 스레드보다 덜 자원 집약적이다.

스레드를 사용할 때 JVM의 가용메모리를 고갈시키는 코드는 자원 한계 발생 없이 코루틴을 사용하여 표현할 수 있다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(50_000) { // launch a lot of coroutines. 코루틴 50000개.
        launch { // 굉장히 적은 양의 메모리를 소비함.
            delay(5000L)
            print(".")
        }
    }
}

위의 코드를 스레드에 맞춰서 표현하면 엄청난 양의 메모레를 소비한다.

OS, JDK 버전, 설정들에 따라서 메모리를 벗어났다는 에러를 발생시키거나 스레드들을 느리게 시작하여 동시에 실행되는 스레드가 절대 너무 많지 않게 된다.