본문으로 바로가기

[Kotlin] 코틀린 - 코루틴#1 기본!

category 개발이야기/Kotlin 2018. 12. 3. 11:22
반응형


이 글은 아래 링크의 내용을 기반으로 하여 설명합니다.

https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md

또한 예제에서 로그 print시 println과 안드로이드의 Log.e()를 혼용합니다.

코루틴의 시작

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // launch 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
}


코루틴의 핵심은 light-weight threads 입니다.

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

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

GlobalScope의 lifetime은 전체 application process에 의존하므로, application이 종료되면 같이 끝나게 되기 때문에 끝에서 sleep을 걸고 기다려야 launch 내부 동작을 실행할 수 있습니다.


단, 위 코드를 안드로이드에서 실행하면 sleep을 걸지 않아도 "Hello world"가 출력됨을 알수 있습니다.

Activity를 finish()하더라도 process 자체가 죽지 않기 때문에 sleep이 없더라도 coroutine 내부 코드는  그대로 실행됩니다.

단! 저가형 단말에서는 memory 문제로 finish()함께 process가 kill될수도 있을테니 반드시 coroutine 동작이 유지된다고 판단할 수도 없습니다.


GlobalScope.lauch{...}thread{...}는 같은 역할, delay{...}Thread.sleep{...}가 같은 역할이라고 이해하시면 빠릅니다.

위 예제는 delay()라는 non-blocking 코드와 sleep()이라는 blocking 코드로 사용되었기 때문에 이를 전부 non-blocking 코드로 치환하면 아래와 같이 할수 있습니다.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // start main coroutine
    GlobalScope.launch { // launch 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을 만나면 main thread는 내부 코드가 완료될때 까지 block됩니다.

fun test2() {
    runBlocking<Unit> {
        launch {
            delay(1000L)
            Log.e(TAG, "World")
        }
        Log.e(TAG, "Hello")
        delay(2000L)
    }
    Log.e(TAG, "End function")
}

Hello

World

End function

Waiting for a job

사실 작업 완료를 기다리기 위해 delay로 대기하는건 별로이기 때문에 실제로는 join을 사용합니다.
join 역시 non-blocking 코드입니다.

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


launch의 return 값으로 Job이 반환됩니다.

이는 Job의 스케쥴 관리나, 상태등을 확인할때 사용합니다.

Structured concurrency

GlobalScope.launch는 top-level coroutine을 만듭니다. 위에서 언급했듯이 GlobalScope은 process가 살아 있는한 계속 유지되기 때문에 GlobalScope에 계속해서 launch를 하는 작업은 메모리면에서나 관리면에서 쉽게 에러를 유발할수 있습니다.

만약 GlobalScope.launch만 사용한다면, 이를 관리하기 위해 reference(Job)을 모두 가지고 있으면서 join을 수동으로 관리해야 합니다.
이는 에러를 발생시키기 쉬운 형태입니다.

이러한 수동작업을 해결하기 위해 Structured concurrency를 사용할 수 있습니다.
이는 특정한 Coroutine scope을 만들고 그 안에서 새로운 coroutine Builder로 coroutine을 시작하면 생성된 CoroutineScope이 코드블럭의 coroutineScope에 더해집니다. 
따라서 외부 scope은 내부의 coroutine들이 종료되기 전까지는 종료되지 않으므로 join 없는 간단한 코드를 만들 수 있습니다.

  • 코루틴 (외부)블럭은 내부에서 실행되는 coroutine이 모두 완료되야만 (외부블럭이)완료된다.

fun test2_1() {
    runBlocking {
        val jobs = List(10) {
            launch {
                delay(1000L)
                Log.e(TAG, "aaa")
            }
        }
        // join을 하고 안하고에 따라 End runBlock이 먼저 찍힐지 끝나고 찍힐지가 결정된다
        // jobs.forEach { it.join() }

        Log.e(TAG, "End runBlock ")
    }
    Log.e(TAG, "End function")
}

위 코드에서는 runBlocking coroutine builder로 function에 coroutine 영역으로 만들었습니다.

그리고 그 안에서 새로운 coroutine을 launch 합니다 ( list를 생성하면서 10개를 launch 시킴)

runBlocking은 내부 coroutine인 list의 열번이 다 수행될때까지 block 되며, lauch로 실행한 비동기 동작(자식)이 모두 끝나야 runBlocking{} 블럭이(부모) 종료 됩니다. 


위 코드 수행시 아래와 같은 결과가 나옵니다.

End runBlock

aaa

aaa

...

End Function


위에서 주석처리된 join() 라인을 넣는다면 아래와 같이 코드 실행 순서가 보장되면서 찍히겠죠?

aaa

aaa

...

End runBlock

End Function

위에서의 join()은 실행 순서를 보장하기 위해 쓰였습니다.


Scope builder

위에는 코드 순서를 보장하기 위해서 내부 coroutine을 join 시켰습니다.

하지만 Coroutine scope을 내부에 만들어서 순서를 보장할 수도 있습니다.
fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking#2")
    }
    
    coroutineScope { // Creates a new coroutine scope
        launch {
            delay(500L) 
            println("Task from nested launch#3")
        }
    
        delay(100L)
        println("Task from coroutine scope#1") // This line will be printed before nested launch
    }
    
    println("Coroutine scope is over#4") // This line is not printed until nested launch completes
}



위 코드를 실행하면 아래와 같은 순서로 로그가 찍힙니다.

Task from coroutine scope#1

Task from runBlocking#2

Task from nested launch#3

Coroutine scope is over#4


coroutineScope을 이용하여 내부에 또다른 scope을 만들었습니다.

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

이런 이유로 "Coroutine scope is over"은 coroutineScope{...}이 끝날때 까지 기다렸다가 찍힙니다.


Extract function refactoring

coroutine에서 사용하는 코드를 외부 함수로 빼내려면 suspend keyword를 사용해야 합니다.
fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

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

suspend function은 내부에서 coroutine api를 사용할수 있습니다.

(예제에서 delay 사용)

다만 doWorld()는 일반 함수에서 호출시 compile error를 발생시킵니다. (coroutine scope에서 사용하라고..)


Coroutines ARE light-weight

아래 코드를 수행시 100K의 coroutine 정상적으로 수행됩니다.


하지만 만약 이걸 thread로 바꾸면...OOM나고 죽습니다.

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


Global coroutines are like daemon threads

GlobalScope에서 수행하는 coroutine은 daemon thread와 같습니다.

이는 process가 kill되면 같이 멈춘다는걸 의미합니다.

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

위 코드 실해시 I'm sleeping.."은 세개만 찍히고 끝납니다.

  • runBlocking은 내부에서 발생한 모든 자식 corouitne의 동작을 보장합니다.
  • 내부에서 GlobalScope을 이용하여 launch 했기 때문에 runBlocking과는 다른 scope을 갖습니다.(runBlockin의 자식 coroutine이 아님)
  • 따라서 runBlock은 1.3초만 대기하고 종료하고, main함수가 종료되면서 application process 역시 종료 됩니다.

단!! 위에서 언급했던것과 같이 안드로이드에서 이 코드를 수행하면 죽지않고 계속 로그가 찍힙니다.

LMK에 의해서 process가 맞가 죽을때 까지요.


반응형