본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[coroutine] Coroutine Basics ( 코루틴 기초 )

by 돼지왕 왕돼지 2020. 3. 7.
반응형




https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/basics.md#coroutine-basics


Your first coroutine


-

fun main(){
    GlobalScope.launch{
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    Thread.sleep(2000L)
}

// Hello,
// World!


-

기본적으로 코루틴은 light-weight thread 이다.

CoroutineScope 라는 context 안에서 "launch" 와 같은 coroutine builder 를 통해 시작된다.

위의 예제에서는 GlobalScope 라는 coroutine 을 사용해서 launch 를 했다.

이 GlobalScope 는 전체 app 생명주기동안 살아있는 녀석이다.



-

만약 GlobalScope.launch { ... } 를 thread { ... } 로 바꾸면 컴파일러는 다음과 같은 에러를 내뱉는다.


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


delay 는 suspend function 이며 Thread.sleep 과는 다르게 thread 를 block 하지 않는다. 대신 coroutine 을 지연시킬 뿐이다.



-

suspend function 은 coroutine 이나 suspend function 에서만 호출되어야 한다는 규칙이 있다.

이 rule 은 기본 지식으로 외우면 된다.





Bridging blocking and non-blocking worlds


-

첫번째 예제는 non-blocking delay { ... } 와 blocking 인 Thread.sleep{ ... } 이 한 코드에 혼용되어 사용되었다.

이제 runBlocking coroutine builder 에 대해서 보자.


fun main(){
    GlobalScope.launch{
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    runBlocking{ // block main thread
        delay(2000L) // JVM 이 살아있게 하기 위해 2초간 delay 한다.
    }
}

// Hello,
// World

runBlocking 은 내부에 있는 coroutine 이 모두 끝날때까지 호출 thread 를 block 시킨다.



-

위 코드는 아래와 같이 쓰일 수도 있다.

fun main() = runBlocking<Unit> {
    GlobalScope.launch{
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    delay(2000L)
}

// Hello,
// World!



-

다음은 suspending function 에 대한 unit test 코드이다.


class MyTest{
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit>{
        // ...
    }
}








Waiting for a job


-

다른 coroutine 의 동작이 끝나기를 delay 로 기다리는 것은 좋은 방법이 아니다.

Job 이라는 녀석을 이용해서 명시적으로 기다릴 수 있다.


fun main() = runBlocking{
    val job = GlobalScope.launch{
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join()
}

// Hello,
// World!


위 코드는 delay 와 동일한 결과를 냈지만, 시간에 구애받지 않는 더 좋은 코드가 되었다.





Structured concurrency


-

GlobalScope.launch 를 사용하게 되면 우리는 top-level coroutine 을 생성하는 것이다.

비롯 light-weight 이기는 하나 그래도 여전히 memory 를 비롯한 res 를 사용한다.

우리가 만약 새롭게 launch 된 coroutine 이 돌고 있다는 사실을 잊는다면, 그 로직은 계속 돌 것이다.

이런 것들이 반복되면 의미없는 코드가 돌거나, 너무 많은 코루틴의 돌아 out of memory 현상이 발생할 수도 있다.

수동으로 모든 launch 된 coroutine 을 참조하고, join 으로 끝나기를 기다리는 것은 에러 발생 가능성도 높다.


그래서 더 좋은 방법을 생각해냈다.

우리는 코드에 구조화된 동기화를 할 수 있다.

GlobalScope 에서 launch 하는 대신, 우리가 threads 를 사용하듯이 우리가 사용하는 context 범위 내에서 coroutine 을 특정한 scope 안에서 launch 하는 것이다.


우리의 예제에서는 main function 이 runBlocking coroutine builder 를 사용하였다.

runBlocking 을 비롯한 모든 coroutine builder 은 code block 에 "CoroutineScope" instance 를 추가한다.

우리는 이 scope 에서 join 을 명시적으로 부를 필요 없이 coroutine 을 launch 시킬 수 있다. 

왜냐하면 바깥쪽 coroutine (예제에서는 runBlocking) 은 해당 scope 에서 launch 된 coroutine 이 모두 종료되기 전까지 종료되지 않기 때문이다.

그래서 우리는 예문을 다음과 같이 변경시킬 수 있다.


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

// Hello,
// World!




Scope builder


-

다른 builder 에 의해 제공되는 coroutine scope 이외에 우리만의 scope 을 "coroutineScope" 이라는 builder 를 통해 만들 수도 있다.

이것은 새로운 coroutine scope 를 만들며 이 scope 에서 launch 된 녀석들이 모두 끝날 때까지 끝나지 않는다.

runBlocking 과 coroutineScope 의 차이점은, coroutineScope 은 현재 thread 를 blocking 하지 않는다는 것이다.


fun main() = runBlocking{
    launch{
        delay(200L)
        println("Task from runBlocking")
    }

    coroutineScope{
        launch{
            delay(500L)
            println("Task from nested launch")
        }

        delay(100L)
        println("Task from coroutine scope")
    }

    println("Coroutine scope is over")
}

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


-

돼왕 : Coroutine scope is over 가 왜 마지막에 찍히는지 정확히 이해되지 않는다면 다음 글을 보자.

[coroutine] coroutineScope 의 동작 특성을 알아보자.





Extract function refactoring


-

launch{ ... } 코드를 다른 function 으로 분리해내보자.

분리해낸 함수는 suspend modifier 를 붙여야 하며, 이것을 suspending function 이라 부른다.

suspending function 은 coroutine 에서 일반 function 처럼 이용될 수 있으며, 이 안에서는 delay 와 같은 다른 suspending function 을 부를 수 있다.


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

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

// Hello,
// World!


-

현재 scope 에서 불리는 coroutine builder 를 가진 함수를 extract 할때는 어떻게 해야 할까?

이 경우에는 suspend modifier 로는 충분하지 않다.

CoroutineScope 의 extension method 로 doWorld 를 만드는 것이 한가지 방법이다. 그러나 이 방법은 API 를 깔끔하게 만들지도 않고 언제나 적용할 수 있는 방법도 아니다.

주로 사용하는 방법은 해당 함수에서 명시적으로 접근할 수 있는 CoroutineScope 를 class 에 field 로 갖게 하거나, outer class 가 CoroutineScope 를 구현한 것을 접근할 수 있게 하는 것이다.

마지막 방법으로 CoroutineScope(coroutineContext) 를 사용할 수 있지만, 이 방법은 구조적으로 안전하지 않다. 왜냐면 이 함수의 실행 scope 를 더 이상 control 할 수 없기 때문이다. 오직 private API 만 이 builder 를 사용할 수 있다.

( 돼왕 : 정확한 이해는 나중에 실 사용하거나, 추후 tutorial 들을 모두 봐야 이해가 좀 가능할 것이다. )








Coroutines ARE light-weight


-

fun main() = runBlocking {
    repeat(10_0000){
        launch{
            delay(1000L)
            printl(".")
        }
    }
}

이 코드를 실행하면 1초마다 10만개의 coroutine 을 launch 하며, 각 coroutine 은 점을 찍는다.

이것을 thread 로 시도해보면, 무슨 일이 생길까? ( 대부분 out of memory error 가 발생할 것이다. )





Global coroutines are like daemon threads


-

다음 코드는 GlobalScope 에서 장수하는 coroutine 을 launch 하며, "I'm sleeping" 을 1초에 2번 프린트한다.

main 은 GlobalScope launch 후, delay 후 return 한다.


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

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

runBlocking 이 1300L ms 후에 종료되면서 app 이 종료되었기 때문에, GlobalScope 의 로직도 종료된다.

GlobalScope 에서 launch 된 active coroutine 은 process 를 alive 하는 역할을 하지 않는다.

데몬 스레드처럼 일반 쓰레드(main 등)이 모두 종료되면 함께 종료된다.







반응형

댓글