ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 공식 가이드 문서로 Coroutine 공부하기 - [01. Coroutine Basics]
    개발/Kotlin 2020. 11. 4. 06:28

     

    [ 01. 코루틴 기본 사항 ]

     


     

    코루틴 기본 개념을 다룹니다.

     

     

    # 첫번째 코루틴


    - code [전체코드 확인하기] : 

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

     

    - result : 

    Hello,
    World!

     

    기본적으로 코루틴은 경량(light-weight) 스레드입니다. 일부 CoroutineScope 컨텍스트에서 launch 코루틴 빌더를 통해 시작합니다. 여기서 우리는 GlobalScope에서 새로운 코루틴을 런칭하는데, 새로운 코루틴의 수명(lifetime)는 전체 애플리케이션의 수명(lifetime)에 의해서만 제한된다는 것을 의미합니다.

     

     GlobalScope.launch { ... } 를  thread { ... } 로,  delay(...) Thread.sleep(...) 로 대신하여도 같은 결과를 얻을 수 있습니다. (단, kotlin.concurrent.thread 를 import해야 합니다.)

     

    만약 GlobalScope.launch 대신 thread 를 사용한다면, 컴파일러는 다음과 같은 에러를 발생시킵니다.

    Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function

     

    delay는 스레드를 차단(block)시키지 않고 코루틴을 일시중단(suspend)시키는 특수한 suspending function으로, croutine에서만 사용할 수 있습니다.

     

     

     

     

    # blocking과 non-blocking간의 연결


    첫번째 예제에서는 동일한 코드 내에 non-blocking 코드인 delay(...)blocking 코드인 Thread.sleep(...) 를 함께 사용하였습니다. 이는 코드가 blocking인지 non-blocking인지 추적하는데 어려움을 줍니다. runBlocking 코루틴 빌더를 사용하여 blocking에 대해 명시적으로 설명하겠습니다.

     

    - code [전체코드 확인하기] :

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

     

    - result : 

    Hello,
    World!

     

    결과는 같으나, 해당 코드는 non-blocking인 delay만을 사용하였습니다. runBlocking 을 호출한 메인스레드는 runBlocking 내부의 코루틴이 완료될 때까지 차단(block)됩니다.

     

     

    해당 예제는 runBlocking 으로 메인함수의 실행코드를 래핑하여, 보다 관용적인 방법으로 재작성할 수 있습니다.

    - code [전체코드 확인하기]: 

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

     

    - result : 

    Hello,
    World!

     

    여기서 runBlocking<Unit> { ... } 는 최상위 레벨(top-level)의 메인 코루틴을 시작하는 데 사용되는 어댑터로 작동됩니다. Kotlin에서 적격한 메인함수는 Unit 타입을 리턴하므로, Unit 리턴타입을 명시적으로 지정합니다.

     

    suspending function의 단위 테스트(unit test)를 작성하는 방법 :

    class MyTest {
        @Test
        fun testMySuspendingFunction() = runBlocking<Unit> {
            // 여기서 우리가 선호하는 어떠한 assertion style을 사용하여 suspending functions을 사용할 수 있다.
        }
    }

     

     

     

     

    # job 기다리기


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

     

    - code [전체코드 확인하기] :

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

     

    - result : 

    Hello,
    World!

     

    결과는 여전히 같으나, 메인 코루틴의 코드는 어떤 식으로든 백그라운드 작업 기간에 얽매이지 않습니다. 한결 나은 방안입니다.

     

     

     

     

    # 구조화 된 동시성 (structured concurrency)


    코루틴의 실질적으로 사용하기 위해서는 아직 아쉬운 점이 남아있습니다. GlobalScope.launch 를 사용할 때, 우리는 최상위 레벨(top-level)의 코루틴을 생성합니다. 이는 가벼운(light-weight) 작업이기는 하나, 실행하는 동안 여전히 일부 메모리 리소스를 소진합니다. 새로 실행한 코루틴에 대한 참조 유지하는 것을 잊더라도 계속해서 실행됩니다. 코루틴의 코드가 중단된다면(예 : 너무 오래 delay되는 경우), 너무 많은 코루틴을 실행시키고 메모리가 부족해진다면 어떻게 될까요? 실행된 모든 코루틴에 대한 참조를 수동으로 유지하고 join하는 것은 오류를 발생시키기 쉽습니다.

     

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

     

    예제에 runBlocking 코루틴 빌더를 사용하여 코루틴으로 변환된 메인 함수가 있습니다. runBlocking을 포함한 모든 코루틴 빌더는 코드 블럭의 범위에 CoroutineScope 인스턴스를 추가합니다. 외부 코루틴(본 예시에서는 runBlocking)은 해당 범위에서 시작된 모든 코루틴이 완료 될 때까지 완료되지 않으므로, 명시적으로 join 할 필요 없이 해당 범위내에서 코루틴을 실행시킬 수 있습니다. 따라서 우리는 예제를 더 간단하게 만들 수 있습니다.

     

    - code [전체코드 확인하기] :

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

     

    - result : 

    Hello,
    World!

     

     

     

     

    # 스코프 빌더 (Scope builder)


    다른 빌더가 제공하는 coroutine scope 외에도 coroutineScope를 사용하여 직접 scope를 선언할 수 있습니다. 이는 coroutine scope를 생성하고 시작된 모든 자식요소가 완료될 때 까지 종료되지 않습니다.

     

    runBlockingcoroutineScope는 body와 모든 자식요소들이 완료될 때까지 기다린다는 점에서 유사해보일 수 있습니다. 가장 큰 차이점은 runBlocking 메서드는 작업을 기다리는 동안 현재 스레드를 차단(block)시키는 반면, coroutineScope는 일시중단되어 다른 작업을 위해 기본 스레드를 해제시킨다는 것입니다. 그 차이 때문에, runBlocking은 일반 함수이고 coroutineScope는 일시중지(suspending) 함수입니다.

     

    다음 예제를 통해 확인 할 수 있습니다.

     

    - code [전체코드 확인하기] : 

    import kotlinx.coroutines.*
    
    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

     

    coroutineScope가 아직 완료되지 않았음에도 "Task from coroutine scope" 메시지(중첩된 launch를 기다리는 동안)가 출력된 바로 직후에 "Task from runBlocking"가 실행되고 출력됩니다.

     

     

     

     

    # 발췌된 함수(Extract function) 리팩토링


    launch { ... } 내부의 코드블럭을 별도의 함수로 발췌하겠습니다. 해당 코드에서 "발췌된 함수(Extract function)" 리팩토링을 수행한다면 suspend 수식어를 가진 새로운 함수가 생성됩니다. 이것이 당신의 첫 번째 suspending function 입니다. Suspending function는 일반 함수와 마찬가지로 코루틴 내에서 사용할 수 있지만, 부가적인 특징은 다른 suspending function(본 예제에서는 delay와 같음)을 차례로 사용하여 코루틴 실행을 중단(suspend) 할 수 있다는 것입니다.

     

    - code [전체코드 확인하기] :

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        launch { doWorld() }
        println("Hello,")
    }
    
    // 이것은 당신의 첫번째 suspending function 입니다
    suspend fun doWorld() {
        delay(1000L)
        println("World!")
    }

     

    - result : 

    Hello,
    World!

     

    그러나 발췌된 함수(Extract function)에 현재 scope에서 호출되는 코루틴 빌더가 포함되어 있다면 어떻게 될까요? 이러한 경우, 발췌된 함수의 suspend 수식어로는 충분하지 않습니다. CoroutineScope에서 doWorld확장(extension)하는 것도 해결방안 중 하나이지만, API를 명확하게 만들지 못하므로 항상 적용할 수 있는 것은 아닙니다. 관용적인 해결방안은 대상 함수를 포함하는 클래스의 필드로 명시적 CoroutineScope를 사용하거나, 외부 클래스가 CoroutineScope를 구현할 때 암묵적으로 갖는 것입니다. 마지막 수단으로 CoroutineScope(coroutineContext)을 사용할 수 있지만, 그러한 접근은 더 이상 해당 메서드의 실행 범위를 제어할 수 없으므로 구조적으로 안전하지 않습니다. Private API만이 해당 빌더를 사용할 수 있습니다.

     

     

     

     

    # 코루틴은 가볍습니다(light-weight)


    다음 코드를 실행하세요.

     

    - code [전체코드 확인하기] : 

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        repeat(100_000) { // 많은 양의 코루틴 실행
            launch {
                delay(5000L)
                print(".")
            }
        }
    }

     

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

    이제 스레드로 시도해보세요. 어떤 일이 발생할까요? (대부분의 코드는 일종의 메모리 부족 에러(out-of-memory)를 발생시킬 가능성이 높습니다.)

     

     

     

     

    # 글로벌 코루틴(Global coroutine)은 데몬 스레드(Daemon thread)와 같습니다


    다음 코드는 GlobalScope에서 "I'm sleeping"을 1초에 두 번 출력하는 장기실행  코루틴을 실행시키고, 약간의 delay 후 메인 함수에서 돌아옵니다.

     

    - 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줄 출력 후 종료되는 것을 확인할 수 있습니다.

    GlobalScope에서 실행된 활성 코루틴은 프로세스를 지속시키지 않습니다. 글로벌 코루틴은 데몬 스레드와 같습니다.

     

     

     

     


     

    Basics - Kotlin Programming Language

     

    kotlinlang.org

     

     

    댓글

Designed by Tistory.