본문 바로가기
개발

코틀린 - 코루틴 기본(Coroutine Basic)

by 마스터누누 2020. 6. 22.
728x90
반응형

첫 번째 코 루틴

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

위의 코드를 IDE에 넣어 실행시켜보자.

 

Hello,
World!

결괏값은 다음과 같다.

 

기본적으로 코루틴은 경량 스레드(light-weight thread)이다. 코 루틴은 Scope 내의 launch 함수에서 실행된다. 위의 예제 코드에서는 코루틴은 Global Scopoe에서 실행되는데, 이는 스레드의 수명이 애플리케이션과 동일함을 의미한다.

 

위 코드에서 GlobalScope.launch {...}를 thread {...}로, delay(...)를 Thread.sleep(...)으로 변경해도 동일한 결과를 얻을 수 있다.

 

그러나 실제로 GlobalScope.launch {...}를 thread {...}로 변경하면 다음과 같은 에러 때문에 컴파일이 실패할 것이다.

 

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

 

이는 코 루틴을 일시 중단시키는 delay 함수 때문인데, 코루틴 내에서만 사용할 수 있기 때문이다.

 

블로킹과 논 블로킹(Blocking and non-blocking)

첫 번째 예제에서는 논블로킹인 depay(...) 함수와 블로킹인 Thread.sleep(...) 함수를 같이 코드에서 사용했다. 이렇게 사용하면 어떤 코드가 블로킹인지 논블로킹인지 파악이 어렵다. 따라서 명시적인 runBlocking 코드를 사용해보았다.

import kotlinx.coroutines.*

fun main() { 
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main thread continues here immediately
    runBlocking {     // but this expression blocks the main thread
        delay(2000L)  // ... while we delay for 2 seconds to keep JVM alive
    } 
}

결괏값은 첫 번째 예제와 동일하지만, 논 블로킹 함수인 delay만 사용되었다. 위 코드는 좀 더 유연한 방식으로 변경이 가능하다.

 

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // start main coroutine
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive
}

runBlocking <Unit> {... }는 최상위 코루틴을 실행하는 어댑터로 사용된다.

 

Job 기다리기(Waiting for a job)

다른 코루틴이 실행되는 동안 작업이 지연되는 건 그리 좋지 않다. 우선 백그라운드 작업이 완료될 때까지(논 블로킹 방식으로) 명시적으로 기다리는 코드를 작성해보자

 

val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
    delay(1000L)
    println("World!")
}
println("Hello,")
job.join() // wait until child coroutine completes

 

결괏값은 같지만 메인 코 루틴은 백그라운드 작업과 분리되어있다. 

 

 

구조화된 동시성(Structed Concurrency)

코루틴의 실용성에는 아직 아쉬운 점이 있다. 우리가 GlobalScope.launch를 사용하면 최상위 레벨의 코루틴이 생성된다. 아무리 경량 스레드라고 할지라도 이는 많은 메모리 리소스를 잡아먹는다. 만약 우리가 이런 코루틴 참조 값을 깜빡한다면 계속 실행되어 리소스를 낭비할 것이다. 코루틴이 중단되는 경우(예를 들면, 동작이 너무 지연되는 경우), 또는 코루틴을 너무 많이 실행시켜 메모리가 부족해진다면 어떻게 될까? 모든 코루틴의 참조값을 관리하는 것은 오류가 발생하기 쉽다.

 

이를 해결하기 위해 우리는 구조화된 동시성을 사용할 수 있다. 바로 Global Scope에서 코루틴을 실행하는 것이 아니라, 우리가 일반적인 스레드를 시작하는것처럼(스레드는 항상 전역적이다.) 특정 작업 영역(Scope)에서 시작하는 것이다.

 

아래 예제의 main 함수에서는 runBlocking 코루틴 빌더를 사용하여 코루틴을 실행시킨다. runBlocking을 포함한 모든 코루틴 빌더는 코드 블록 스코프에 CoroutineScope 인스턴스를 추가한다. 이 스코프 내에서는 명시적으로 join을 사용하지 않아도 코 루틴 호출이 가능하다. 왜냐하면 스코프내에서 실행된 모든 코루틴이 완료되기 전까지는 외부 코루틴도 완료되지 않기 때문이다.

 

따라서 예제를 좀 더 간단하게 만들 수 있다.

 

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine in the scope of runBlocking
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

 

스코프 빌더(Scope builder)

다른 빌더가 제공하는 코루틴 스코프 외에도 coroutineScope 빌더를 사용하여 코루틴의 고유한 스코프를 선언할 수 있다. 이렇게 생성된 스코프는 실행된 모든 하위 코 루틴이 완료되어야 끝이 난다. runBlocking과 coroutineScope는 범위 내의 모든 코루틴과 하위 코루틴들이 완료될 때까지 기다린다는 점에서 비슷하다. 둘의 가장 큰 차이점은 runBlocking은 현재 스레드를 대기시키고, coroutineScope는 중단되고 다른 스레드를 사용한다는 점이다. 이러한 차이점 때문에 runBlocking은 일반 함수이고 coroutineScope는 suspend 함수이다

 

이러한 속성은 다음 예제를 통해 설명할 수 있다.

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }
    
    coroutineScope { // Creates a coroutine scope
        launch {
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") // This line will be printed before the nested launch
    }
    
    println("Coroutine scope is over") // This line is not printed until the nested launch completes
}

"Task from coroutine scope"가 출력된 후에 "Task from runBlocking"이 실행된다.

 

메서드 추출 리팩토링

이제 위의 예제의 launch {} 함수 안에 있는 코드들을 추출하여 별도의 함수로 리팩터링 해보자. 이른바 "함수 추출"이라고 불리는 이 리팩토링 기법을 적용하기 위해, suspend 키워드가 붙은 새로운 함수를 생성해야 한다. 이것이 우리의 첫 번째 suspend fuction이 될 것이다. suspend 함수는 일반 함수처럼 코루틴 내에서 사용이 가능하다. 그러나 일반 함수와 다른 점은 다른 suspend 함수(예를 들면, delay 함수)를 사용하여 코루틴을 일시적으로 중단시킬 수 있다는 것이다.

 

 

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

 

코루틴은 Light-weight이다.

아래의 코드를 실행시켜 보자

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(1000L)
            print(".")
        }
    }
}

위의 코드는 100,000개의 코루틴을 실행하고 각 코 루틴은 "."을 출력한다.

이제 동일한 결괏값을 같은 코드를 스레드로 실행시켜보자. 아마도 메모리 부족 오류가 발생할 것이다.

 

글로벌 코루틴(Global coroutine)은 데몬 스레드(Demon thread)와 같다.

아래의 코드는 GlobalScope에서 동작하는 코 루틴으로써 "I'm sleeping"이라는 문자를 1초에 두 번 출력한다. 이후, 약간의 딜레이 후 메인 함수로 돌아오게 된다.

GlobalScope.launch {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}
delay(1300L) // just quit after delay

실행의 결괏값으로 아래와 같은 문자열을 3줄 입력하고 종료되는 것을 확인할 수 있다.

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

GlobalScope에서 시작된 코루틴은 프로세스를 유지하지 않고, 이점은 데몬 스레드와 같다.

반응형

댓글