Why Coroutine is light-weight thread

코틀린 공식 문서에서는 코루틴을 경량 스레드(light-weight thread)라고 표현하고 있는데, 그 이유를 알아보자.

코루틴은 스레드에서 실행된다. 이것은 대원칙이다. 코루틴이 단독으로 또는 다른 수단에 의해 실행될 수 없다는 것을 명심하자. 그리고 여러개의 코루틴을 하나의 스레드를 지정하여 실행할 수 있지만 동시에 실행 하는 것은 불가능하고, 한번에 하나의 명령어만 실행할 수 있다. 그 이유는 코루틴은 스레드내에서 실행되고 중단 지점(suspension point)에 도달하자마자 스레드를 떠나 대기중인 다른 코루틴을 선택할 수 있도록 해방하기 때문이다. 이렇게하면 스레드와 메모리 사용량이 줄어들어 많은 동시성 작업을 수행할 수 있게 된다.

좀 더 낮은 관점에서 살펴보면 코틀린의 코루틴은 스택리스 코루틴(Stackless Coroutine)이다. 스택리스 코루틴은 스택이 없다는 의미다. 또한 특정 스레드에 종속되지도 않는다. 그렇기 때문에 프로세서에서 Context Switching이 필요하지 않다. 이러한 이유들로 인해 수천개의 스레드를 생성하는 것보다 수천개의 코루틴을 생성하는 것이 더 빠르고, 자원을 적게 사용한다. 

다음 코드를 통해 코루틴의 무게감을 느껴보자

@Test
fun coroutineTest() {
    runBlocking {
        println("시작::활성화 된 스레드 갯수 = ${Thread.activeCount()}")
        val time = measureTimeMillis {
            val jobs = ArrayList<Job>()
            repeat(10000) {
                jobs += launch(Dispatchers.Default) {
                    delay(1000L)
                }
            }
            println("끝::활성화 된 스레드 갯수 = ${Thread.activeCount()}")
            jobs.forEach { it.join() }
        }
        println("Took $time ms")
    }
}

실행결과:
시작::활성화 된 스레드 갯수 = 2
끝::활성화 된 스레드 갯수 = 11
Took 1131 ms

Note : measureTimeMillis는 코드 블럭을 갖는 인라인 함수로써 실행시간(ms)을 반환한다.

coroutineTest의 launch {…} 부분을 코루틴 대신 스레드를 사용하는 코드로 대체해보자.

@Test
fun threadTest() {
    runBlocking {
        println("시작::활성화 된 스레드 갯수 = ${Thread.activeCount()}")
        val time = measureTimeMillis {
            val jobs = ArrayList<Thread>()
            repeat(10000) {
                jobs += Thread {
                    Thread.sleep(1000L)
                }.also { it.start() }
            }
            println("끝::활성화 된 스레드 갯수 = ${Thread.activeCount()}")
            jobs.forEach { it.join() }
        }
        println("Took $time ms")
    }
}

실행결과:
시작::활성화 된 스레드 갯수 = 2
끝::활성화 된 스레드 갯수 = 2282
Took 4123 ms
단순 수치상으로는 코루틴이 약 4배정도 빠른 모습을 보여준다. 아무래도 threadTest() 함수의 경우 스레드를 생성하는 과정에서 메모리를 할당하고 해제하는 부분의 비용이 많이 들기 때문일 것이다.

또한 활성화된 스레드 카운트의 갯수를 보면 coroutineTest()는 단 두개의 스레드만 활성화 되어있지만, threadTest()는 2282개의 스레드가 활성화 된 것을 확인할 수 있다.

만약 메모리가 작은 환경이라면 스레드를 생성하다가 다음과 같은 OOM 에러가 발생할 수도 있었다.
[java.lang.OutOfMemoryError: unable to create new native thread]

임의로 OOM을 발생시키는 테스트를 원한다면 repeat 카운트를 100000이상, 스레드의 sleep 시간을 100000이상으로  변경해보자.

한가지 더 테스트를 해보자. 코루틴은 특정 스레드에 매핑되지 않는데, 위의 예제를 이용하여 이 점을 실험해보자.

@Test
fun coroutineTest() {
    runBlocking{
        val time = measureTimeMillis {
            val jobs = ArrayList<Job>()
            for(i in 1..3) {
                jobs += launch(Dispatchers.Default) {
                    println("Start #$i::${Thread.currentThread().name}")
                    delay(1000L)
                    println("End   #$i::${Thread.currentThread().name}")
                }
            }
            jobs.forEach { it.join() }
        }
        println("Took $time ms")
    }
}

코드를 살짝 고쳤다. repeat()를 for문으로 변경하여 3번만 반복하도록 했고, delay를 호출하기 전과 후로 스레드의 이름이 어떻게 다른지 알아보기 위해 메시지를 출력한다.

실행 결과:
Start #1::DefaultDispatcher-worker-1 @coroutine#2
Start #2::DefaultDispatcher-worker-2 @coroutine#3
Start #3::DefaultDispatcher-worker-3 @coroutine#4
End   #2::DefaultDispatcher-worker-2 @coroutine#3
End   #3::DefaultDispatcher-worker-1 @coroutine#4
End   #1::DefaultDispatcher-worker-3 @coroutine#2
Took 1015 ms

delay() 함수 호출후 다른 스레드에서 다시 시작하는 경우를 확인할 수 있다.

스레드는 한 번에 하나의 코루틴만 실행할 수 있기 때문에, 시스템 필요에 따라 코루틴을 스레드들 사이에서 옮기게 된다. 

 

Buy me a coffeeBuy me a coffee
카테고리: AndroidKotlin

2개의 댓글

오타가 있어요 · 2021년 5월 6일 12:33 오전

좀 더 낮은 관점에서 살펴보면 코루틴의 코루틴은 스택리스 코루틴(Stackless Coroutine)이다. 스택리스 코루틴은 스택이 없다는 의미다.
코루틴의 코루틴 -> 코틀린의 코루틴 인거 같아요

    Charlezz · 2021년 5월 6일 10:13 오전

    제보 감사합니다~

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.