반갑습니다!

[Kotlin] Coroutine Basics 본문

Kotlin

[Kotlin] Coroutine Basics

김덜덜이 2020. 7. 4. 16:55

Your first coroutine

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 백그라운드에서 새로운 코루틴 실행
        delay(1000L) // 1초간 논블로킹 딜레이
        println("World!")
    }
    println("Hello,") // 코틀린이 지연되는 동안 메인 쓰레드 실행
    Thread.sleep(2000L) // 2초 동안 프로그램이 살아있음
}

/* 실행결과
Hello,
World!
*/

위 코드를 실행해보면 쓰레드를 실행한 것처럼 동작함을 알 수 있다. 여기서 GlobalScope로 생성된 블럭을 'Coroutine Scope' 라고 한다. 그리고 launch를 통해 코루틴을 실행시키는데, 이러한 역할을 하는 것을 'Coroutine Builder'라고 한다.

GlobalScope는 이름 그대로 전역 범위를 갖기 때문에 프로그램과 생명주기를 같이한다.

위의 예제에서는 delay()Thread.sleep()을 함께 사용하였다. 하지만 이런식으로 코드를 작성하다보면 실행 흐름을 파악하기가 어려워진다는 단점이 있다. 그렇다고 해서 Thread.sleep()delay()로 대체하게되면 컴파일 에러가 발생하는데, 이는 delay()는 코루틴을 중단시키는 목적으로 구현된 suspend 함수이기 때문이다. 즉, suspend함수는 코루틴 스코프 안에서만 사용할 수 있다. 이를 개선하기 위해 또 다른 코틀린 빌더인 runBlocking을 사용하였다.

Bridging blocking and non-blocking worlds

import kotlinx.coroutines.*

fun main() { 
    GlobalScope.launch { // 백그라운드에서 새로운 코루틴 실행
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 메인 쓰레드 코루틴은 즉시 실행 
    runBlocking {     // runBlocking이 메인 쓰레드를 블로킹
        delay(2000L)  // 2초 동안 프로그램이 살아있음
    } 
}

/* 실행결과
Hello,
World!
*/

처음 코드와 결과는 동일하지만 논블로킹 함수인 delay)() 만 사용하였다. runBlocking을 호출하는 메인 쓰레드는 runBlocking이 완료될 때까지 블록된다. 하지만 이러한 코드는 관용적인 코드가 아니다. 이를 조금 수정하면 다음과 같다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // 메인 코루틴 시작
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    delay(2000L)     
}

/* 실행결과
Hello,
World!
*/

하지만 다른 코루틴이 끝날 때까지 임의의 시간동안 기다리는 것은 좋은 방법이 아니다. 다른 코루틴이 언제 끝날지 알 수 없는 경우 정상적인 작동을 보장할 수 없다는 문제가 있기 때문이다. 이번에는 임의의 시간이 아니라 코루틴이 끝날 때까지 기다려보자.

Waiting for a job

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = GlobalScope.launch { // Job객체에 대한 참조를 유지하면서 코루틴 실행
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // 자식 코루틴이 끝날 때까지 기다린다.
}

/* 실행결과
Hello,
World!
*/

launch 를 하게되면 Job객체를 반환하고 실행된다. 따라서 코루틴이 실행되는 동안에도 Job 객체에 대한 참조를 유지할 수 있다. 그리고 join() 을 사용하면 해당 Job 객체의 작업이 끝날 때가지 기다릴 수 있다.

Structured concurrency

이번엔 코루틴을 여러개 생성해보자.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job1 = GlobalScope.launch { // Job객체에 대한 참조를 유지하면서 코루틴 실행
        delay(1000L)
        println("World!")
    }
      val job2 = GlobalScope.launch { // Job객체에 대한 참조를 유지하면서 코루틴 실행
        delay(1000L)
        println("World!")
    }
      val job3 = GlobalScope.launch { // Job객체에 대한 참조를 유지하면서 코루틴 실행
        delay(1000L)
        println("World!")
    }
      val job4 = GlobalScope.launch { // Job객체에 대한 참조를 유지하면서 코루틴 실행
        delay(1000L)
        println("World!")
    }
      val job5 = GlobalScope.launch { // Job객체에 대한 참조를 유지하면서 코루틴 실행
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job1.join()
    job2.join()
    job3.join()
    job4.join()
    job5.join()
}

/* 실행결과
Hello,
World!
World!
World!
World!
World!
*/

이런식으로 코루틴이 늘어나면 늘어날수록 join() 함수를 계속해서 생성해야하기 때문에 좋지 않다. 이는 메인 코루틴과 GlobalScope 로 선언된 코루틴 사이의 구조적인 관계가 없기 때문이다. 이는 코루틴들 간에 구조적인 관계를 만들어줌으로써 개선할 수 있다.

import kotlinx.coroutines.*

fun main() = runBlocking { // 코루틴 스코프
    launch {
        delay(1000L)
        println("World!")
    }
    launch {
        delay(1000L)
        println("World!")
    }
      launch {
        delay(1000L)
        println("World!")
    }
      launch {
        delay(1000L)
        println("World!")
    }
      launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

/* 실행결과
Hello,
World!
World!
World!
World!
World!
*/

메인 함수는 runBlocking 코루틴 빌더를 통해 코루틴에서 실행되고, 그 안에서 5개의 코루틴이 생성되었다. runblocking으로 생성된 코루틴은 자식 코루틴이 종료될 때까지 종료되지 않기 때문에 위의 예제는 정상적으로 작동한다.

Extract function refactoring

이번엔 "World!" 를 출력하는 부분을 별도의 함수로 분리해보자. 위에서 설명했듯이 delay()는 코루틴 스코프에서만 사용할 수 있기 때문에 suspend 키워드를 사용해서 함수를 작성해야 한다.

import kotlinx.coroutines.*

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

suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

/* 실행결과
Hello,
World!
*/

Coroutines ARE light-weight

코루틴은 쓰레드와 비슷하게 동작하지만 쓰레드보다 가볍다는 특징이 있다. 다음 예제를 실행해보며 직접 확인해보자.

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // 100000개의 코루틴 생성
        launch {
            delay(1000L)
            print(".")
        }
    }
}

온점 10만개가 무리 없이 출력된다. 이번엔 동일한 코드를 쓰레드로 변경해보자.

import kotlinx.coroutines.*
import kotlinx.concurrent.thread

fun main() = runBlocking {
    repeat(100_000) {
         thread { 
             Thread.sleep(1000L)
             println(".")       
        }
    }
}

실행해보면 코루틴이 쓰레드보다 가볍다는 것을 알 수 있을 것이다.

Global coroutines are like daemon threads

import kotlinx.coroutines.*

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

/* 실행결과
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
*/

GlobalScope는 어플리케이션과 수명을 같이하기 때문에 어플리케이션이 종료되면 코루틴도 같이 종료된다.

'Kotlin' 카테고리의 다른 글

[Kotlin] Composing Suspending Functions  (0) 2020.07.05
[Kotlin] Cancellation and Timeouts  (0) 2020.07.04
[Kotlin] Coroutine Guide  (0) 2020.07.04
[Kotlin] 반복문  (0) 2020.06.15
[Kotlin] 조건문  (0) 2020.06.15