ABOUT ME

-

  • Kotlin) Coroutine 공식 가이드 번역 01 - Basics
    Kotlin 2021. 1. 8. 14:37

    코루틴 공부를 위해 코루틴 공식 문서 순서에 따라 글을 작성해 보려고 합니다.

    Coroutines?

    우선 코루틴이 무엇인지에 대해 간략하게 알아보고 가겠습니다.

    • 협력형 멀티 태스킹
    • 동시성 프로그래밍 지원
    • 비동기 처리를 쉽게 도와준다.

     

    ▶ 코루틴의 핵심은 경량 스레드 입니다.


    코루틴의 시작

    code

    import kotlinx.coroutines.GlobalScope
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    
    fun main() {
        GlobalScope.launch { // 새로운 코루틴을 백그라운드에서 실행 및 진행
            delay(1000L) // 1초간 non-blocking 지연 (기본 시간 단위는 밀리세컨드)
            println("World!") // delay 후, 출력
        }
        println("Hello,") // 코루틴이 지연(delay)되는 동안 메인스레드는 계속하여 진행
        Thread.sleep(2000L) // JVM 유지를 위해 2초간 메인 스레드 차단(block)
    }
    

    result

    Hello,
    World!

    위의 코드는 메인 함수 안에서 GlobalScope.launch { } 코드 블록을 이요하여 Hello, World! 를 출력하는 예제입니다.

    launch는 코루틴의 빌더이며 이를 이용해서 coroutineScope 안에서 실행시킵니다.

    위 예제에서는 GlobalScope에서 코루틴을 실행했습니다.

     

    가장 마지막 라인에서 2초가 정지(sleep)하는 코드가 쓰인 이유GlobalScope와 관련되어 있습니다.

    GlobalScope의 lifetime이 전체 Application에 의존하기 때문에, Application이 종료되면 같이 끝나게 돼서 끝에서 sleep을 걸고 기다려야 launch 내부 동작을 실행할 수 있습니다. 이렇게 스레드를 멈추는 역할을 수행하는 함수를 중단 함수(Blocking function)라고 합니다.

     

    GlobalScope.launch {..}thread {..}, delay {..}Thread.sleep {..}은 같은 역할이라고 이해하시면 됩니다.

    이러한 중단 함수가 현재 스레드를 멈추게 할 수 있다는 것을 코드상에서 보다 명확하게 나타내기 위해 runBlocking { } 블록을 사용해서 코드를 작성할 수 있습니다.

     

    ※안드로이드에서 위 코드를 실행하면 sleep을 걸지 않아도 "Hello World"가 출력됩니다.

    액티비티의 finish()를 호출하더라도 process 자체가 죽지 않기 때문입니다.


    blocking과 non-blocking 간의 연결

    code

    import kotlinx.coroutines.GlobalScope
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    fun main() { 
        GlobalScope.launch { // 새로운 코루틴을 백그라운드에서 실행 및 진행
            delay(1000L)
            println("World!")
        }
        println("Hello,") // 메인 스레드는 곧바로 이 코드를 실행
        runBlocking {     // 해당 코드는 JVM 유지를 위해 2초간 지연(delay)하는 동안
            delay(2000L)  // ... 메인스레드를 차단(block)
        } 
    }

    result

    Hello,
    World!

    결과는 위와 같습니다.

    위의 코드는 non-blocking에 delay만 사용했습니다.

     

    runBlocking을 만나면 메인 스레드는 내부 코드(코루틴)가 완료될 때까지 block 됩니다.

    위의 예제를 보다 관용적인 방법으로 아래와 같이 작성할 수 있습니다.

    code

    import kotlinx.coroutines.GlobalScope
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    fun main() = runBlocking { // 메인 코루틴 시작
        GlobalScope.launch { // 새로운 코루틴을 백그라운드에서 실행 및 진행
            delay(1000L)
            println("World!")
        }
        println("Hello,") // 메인 코루틴은 곧바로 이 코드를 실행
        delay(2000L) // JVM 유지를 위해 2초간 지연(delay)
    }
    

    result

    Hello,
    World!

    job 기다리기

    다른 코루틴이 작업하는 동안 지연되는 것은 좋은 방식이 아닙니다. 시작된 백그라운드 job이 완료될 때까지(non-blocking 방식으로) 명시적으로 대기합니다.

    code

    val job = GlobalScope.launch { // 새로운 코루틴을 시작하고 해당 Job에 대한 참조를 유지
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // 하위 코루틴이 완료될 때까지 대기

    result

    Hello,
    World!

    결과는 위와 같습니다. launch의 retrun값으로 job이 반환됩니다.

    메인 코루틴의 코드는 어떤 식으로든 백그라운드 작업 기간에 얽매이지 않습니다.

     

    사실 작업 완료를 기다리기 위해 delay로 대기하는 건 별로이기 때문에 실제론 join을 사용합니다.

    join 역시 non-blocking 코드입니다.


    구조화된 동시성

    코루틴을 실질적으로 사용하기 위해서는 아직 아쉬운 점이 있습니다. GlobalScope.launch를 사용할 때, 우리는 최상위 레벨의 코루틴을 생성합니다.

    이것은 가벼운 작업이긴 하나, 실행하는 동안 여전히 일부 메모리 리소스를 소진합니다.

    위에서 언급했듯이 GlobalScope는 process가 살아 있는 한 계속 유지되기 때문에 새로 실행한 코루틴에 대한 참조 유지하는 것을 잊더라도 계속해서 실행됩니다. 그래서 메모리면이나 관리면에서 쉽게 에러를 유발할 수 있습니다.

     

    더 나은 해결방법으로 코드에서 structured concurrency를 사용할 수 있습니다. GlobalScope에서 코루틴을 시작시키는 대신, 우리가 보통 스레드로 하는 것처럼 수행하는 작업의 특정 범위 내에서 코루틴을 시작할 수 있습니다.

    code

    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    fun main() = runBlocking { /// this: CoroutineScope
        launch { // runBlocking 범위 내에서 새로운 코루틴 실행
            delay(1000L)
            println("World!")
        }
        println("Hello,")
    }
    

    result

    Hello,
    World!

    위의 예제는 runBlocking 코루틴 빌더를 사용하여 코루틴으로 변환된 메인 함수가 있습니다. runBlocking을 포함한 모든 코루틴 빌더는 코드 블럭의 범위에 CoroutineScope 인스턴스를 추가합니다.

     

    코루틴 외부 블록은 내부에서 실행되는 코루틴이 모두 완료되어야만 완료됩니다.

    외부 코루틴(예제에서는 runBlocking)은 해당 범위에서 시작된 모든 코루틴이 완료될 때까지 완료되지 않으므로, 명시적으로 join 할 필요 없이 해당 범위 내에서 코루틴을 실행시킬 수 있습니다.


    스코프 빌더

    다른 빌더가 제공하는 coroutine scope 외에도 coroutineScope를 사용하여 직접 scope를 선언할 수 있습니다.

    이는 coroutinScope를 생성하고 시작된 모든 자식 요소가 완료될 때까지 종료되지 않습니다.

     

    runBlocking vs coroutinScope

    • body와 모든 자식 요소들이 완료될 때까지 기다린다는 점에서 유사해 보일 수 있습니다.
    • 하지만, runBlocking은 작업을 기다리는 동안 현재 스레드를 block 시키는 반면, coroutineScope는 일시 중단되어 다른 작업을 위해 기본 스레드를 해제시킵니다.
    • 그래서 runBlocking은 일반 함수이고 coroutineScope는 일시중지 함수입니다.

    code

    import kotlinx.coroutines.coroutineScope
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    fun main() = runBlocking { /// this: CoroutineScope
       launch {
           delay(200L)
           println("Task from runBlocking")
       }
        coroutineScope { // coroutine scope 생성
            launch {
                delay(500L)
                println("Task from nested launch")
            }
            delay(100L)
            println("Task from coroutine scope") // 해당 코드라인은 중첩된 launch 이전에 출력됩니다.
        }
        println("Coroutine scope is over") // 해당 코드라인은 중첩된 launch의 작업이 완료될 때까지 출력되지 않습니다.
    }
    

    result

    Task from coroutine scope
    Task from runBlocking
    Task from nested launch
    Coroutine scope is over

    위에서 모든 coroutine block은 내부 코루틴이 모두 완료될 때까지 대기한다고 했습니다.

    그래서 coroutineScope가 아직 완료되지 않았음에도 "Task from coroutine scope" 메시지가 출력된 바로 직후에 "Task from runBlocking" 메시지가 실행되고 출력됩니다.


    발췌된 함수 리팩토링

    코루틴에서 사용하는 코드를 외부 함수로 빼내려면 suspend 키워드를 사용해야 합니다.

    suspend function은 내부에서 coroutine api를 사용할 수 있습니다. (예제에서 delay 사용)

     

    launch{ } 내부의 코드 블록을 별도의 함수로 발췌하겠습니다. 해당 코드에서 리팩토링 한다면 suspend 접두어를 가진 새로운 함수가 생성됩니다. 이것이 첫 번째 suspending function입니다.

    suspending function은 일반 함수와 마찬가지로 코루틴 내에서 사용할 수 있지만, 부가적인 특징은 다른 suspending function을 차례로 사용하여 코루틴 실행을 중단할 수 있다는 것입니다.

    code

    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    fun main() = runBlocking { 
        launch { doWorld() }
        println("Hello,")
    }
    
    suspend fun doWorld() {
        delay(1000L)
        println("World!")
    }
    

    result

    Hello,
    World!

    발췌된 함수에 현재 scope에서 호출되는 코루틴 빌더가 포함되어 있다면 어떻게 될까요?

    이 경우, 발췌된 함수의 suspend 접두어로 충분하지 않습니다. 관용적인 방안으로 대상 함수를 포함하는 클래스의 필드로 명시적 CoroutineScope를 사용하거나, 외부 클래스가 CoroutineScope를 구현할 때 암묵적으로 갖는 것입니다.


    코루틴은 가볍습니다.

    code

    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.runBlocking
    
    fun main() = runBlocking {
        repeat(100_000) {
            launch {
                delay(5000L)
                print(".")
            }
        }
    }
    

    10만 개의 코루틴을 실행하고 5초 후에 각 코루틴이 "."을 출력합니다.

    이것을 스레드로 실행한다면 메모리 부족 에러가 발생할 가능성이 높습니다.


    글로벌 코루틴은 데몬 스레드와 같습니다.

    이것은 process가 죽으면 같이 멈춘다는 것을 의미합니다.

    code

    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // delay후, 곧바로 종료

    result

    I'm sleeping 0 ...
    I'm sleeping 1 ...
    I'm sleeping 2 ...

    코드 실행 후, 3줄 출력 후 종료되는 것을 확인할 수 있습니다.

    • runBlocking은 내부에서 발생한 모든 자식 코루틴의 동작을 보장합니다.
    • 내부에서 GlobalScope를 이용하여 launch 했기 때문에 runBlocking과 다른 scope를 갖습니다.
    • 따라서 runBlocking은 1.3초만 대기하고 종료하고, 메인 함수가 종료되면서 applcation process 역시 종료됩니다.

     

    GlobalScope에서 실행된 활성 코루틴은 프로세스를 지속시키지 않습니다.

     

    ※ 데몬 스레드 : 데몬 스레드는 데몬 스레드가 아닌 스레드, 즉 일반 스레드 Normal Thread의 작업을 돕는 보조적 역할을 담당하는 스레드입니다.

    반응형

    댓글

Designed by Me.