코루틴(Coroutine) vs 쓰레드(Thread)

|

Coroutine is ‘Light-weight Thread’

흔히 코루틴은 Light-weight Thread라고 합니다. 이 말이 정확히 어떤 것을 의미하는지 좀 더 자세히 알아보도록 하겠습니다.


동시성(Concurrency)과 병렬성(Parallelism)

  • 동시성(Concurrency): 다수의 Task를 수행하기 위해서 각 Task를 조금씩 나누어서 실행하는 시분할 방식
  • 병렬성(Parallelism): 다수의 Task를 동시에 실행하는 것

동시성은 각 Task를 조금씩 나누어서 실행하는 것이기 때문에 총 실행 시간은 각 Task의 실행 시간을 합친 것과 같습니다. 예를 들어 10분짜리 Task 5개를 실행한다면, 총 수행 시간은 5 x 10 = 50분이 됩니다. 여기에 각 Task간 작업 전환을 위한 Context Swithcing이 추가로 발생합니다.

병렬성은 Task간 전환이 없기 때문에 Context Switching이 발생하지 않습니다. 대신 자원이 Task 수 만큼 필요합니다. 총 수행 시간은 가장 시간이 긴 Task 만큼 소요됩니다.


Coroutine & Thread for Concurrency

Coroutine과 Thread는 둘 다 동시성을 보장하기 위한 기술입니다. Thread는 OS 레벨에서 각 작업의 동시성을 위해 Preemtive Scheduling을 해서 각 작업을 조금씩 나누어서 실행합니다. Coroutine도 동시성을 목표로 하고 있지만, 각 작업에 Thread를 할당하는 것이 아니라 작은 Object 만을 할당한 다음 이 Object를 스위칭하면서 Context Switching 비용을 최대한 줄였습니다. 그래서 Light-weight Thread라고 부릅니다.


Thread

  • Thread는 각 Task 마다 Thread를 할당합니다.
  • 각 Thread는 자체적인 Stack 메모리를 가지며 JVM Stack 영역을 가집니다.
  • OS 커널에서 Context Switching을 해서 동시성을 보장합니다.
  • 만약 복수의 Thread를 사용해서 Thread 1Thread 2의 결과를 기다려야 한다면, Thread 1은 그 때까지 Blocking 되어 해당 자원을 사용할 수 없습니다.


Coroutine

  • Task 마다 각각 Object를 할당합니다.
  • 각 Coroutine Object는 JVM Heap에 적재됩니다.
  • 커널 레벨의 Context Switching이 아니라 프로그래머가 컨트롤하는 Switching을 통해 동시성을 보장합니다.
  • Task 1 작업을 수행하다가 suspend 되더라도, 해당 Thread 는 유효하기 때문에 Task 2를 같은 Thread에서 실행할 수 있습니다.
  • 하나의 Thread에서 다수의 Coroutine Object를 실행할 수 있으며, 이 경우 Coroutine Object 교체만 발생하기 때문에 커널 레벨의 Context Switching이 발생하지 않습니다.

만약 여러 Thread에서 다수의 Coroutine을 실행할 경우에는 Thread 전환이 일어날 경우 Context Switching이 발생합니다. Coroutine의 No Context Switching 장점을 살리기 위해서는 하나의 Thread에서 복수의 Coroutine Object를 실행하는 것이 유리합니다.

Coroutine은 기존의 Thread를 좀 더 작은 단위로 쪼개어 사용할 수 있는 개념입니다.

하나의 Thread에서 복수의 Coroutine이 실행될 경우 각 Thread가 가지는 Stack 메모리 영역도 하나가 되어 메모리 절약이 되며, 공유 메모리 접근으로 발생할 수 있는 Deadlock 문제도 해결될 수 있습니다.


Stackful & Stackless

Coroutine은 크게 Stackful 방식과 Stackless 방식으로 나눌 수 있습니다. Kotlin의 경우는 Stackless 방식이기 때문에 약간의 기능 제한이 있습니다.

Thread의 경우 자체 Stack 메모리 영역을 가지기 때문에 Stack을 이용해서 함수를 실행하고 관리할 수가 있습니다.

  • Stackful Coroutine : 코루틴 내부에서 다른 함수를 호출할 수 있고 값을 리턴하거나 suspend 할 수 있습니다.
  • Stackless Coroutine : caller에게 항상 무엇인가를 리턴해야 하며, 값을 리턴하거나 no result yet, I'm supended를 리턴합니다.

다시 요약하면, Stackless Coroutine는 항상 값이나 결과를 리턴해야 하기 때문에 코루틴을 호출한 caller가 그 값을 이용해서 판단 및 제어를 해야 하고, Stackful Coroutine는 일반적인 Thread처럼 스스로 suspend도 할 수 있으며 값을 리턴할 수도 있습니다.

각 언어별 지원하는 코루틴 정보는 다음과 같습니다.

  • Stackful Coroutine: Javaflow, Quasar
  • Stackless Coroutine: Kotlin, Scala, C#


suspend

하나의 Thread는 여러 개의 Coroutine을 실행할 수 있습니다. 이 때 Context Switching이 발생하지 않기 때문에 Light-weight Thread라고 부릅니다. 하나의 Thread에서 여러 개의 Coroutine가 실행될 때, 실행 중이던 하나의 코루틴이 suspend(멈춤) 상태가 되면, 해당 Thread에서는 해당 Thread 내의 resume할 다른 코루틴을 찾습니다.

따라서 코루틴 내에서 호출하는 멈출 수 있는 함수는 suspend 키워드를 이용해서 선언할 수 있습니다.

suspend fun getDataFromServer() : Data {
    // TODO
}

위와 같은 함수는 suspend function이 호출되는 순간 해당 코루틴을 잠시 중단시켜놓을 수 있으며 결과 값이 오면 해당 함수를 다시 resume 시킵니다.

코루틴(Coroutine) Dispatchers.Main 사용하기

|

Dispatchers.Main

Dispatchers.Main는 해당 CoroutineScope을 Main UI Thread에서 동작시키도록 합니다.

안드로이드에서 Dispatchers.Main을 그냥 사용하려고 하면 다음과 같은 에러가 발생하는데,

java.lang.ILLegalStateException: Module with Main dispatcher is missing. Add dependency with required Main dispatcher, e.g. 'kotlinx-coroutines-android'

gradle에 다음 라인을 추가해주면 됩니다.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1'

Dispatchers.Main을 이용해서 코루틴을 구동시키면 Main UI Thread에서 동작하기 때문에, Thread.currentThread().id 값을 확인해보면 같은 id 값을 가지는 것을 확인할 수 있습니다.

또한 시간이 오래 걸리는 작업 등을 동작시킬 경우 ANR(Application Not Responding)과 같은 오류가 발생할 수도 있으니 주의해야 합니다.

코루틴(Coroutine) 간략 설명

|

코루틴의 구성 요소

코틀린은 크게 다음과 같은 요소들로 구성되어 있습니다.

  • CoroutineScope
  • CoroutineContext
  • Dispatcher


CoroutineScope

CoroutineScope는 코루틴이 실행되는 범위입니다. 간편하게 사용하는 GlobalScope의 경우 CoroutineScope의 한 종류입니다.


CoroutineContext

CoroutineContext는 안드로이드에서의 Context와 비슷한 역할을 한다고 생각하면 됩니다. 코루틴을 실행하는 제어 정보가 들어있으며, 주요 요소로는 Job이나 Dispatcher가 있습니다. 코루틴을 사용하다보면 대부분 CoroutineScopeDispatcher 위주로 사용하다보니, CoroutineContext는 직접적으로 사용하는 경우는 적을 수도 있습니다.


Dispatcher

어떤 Thread를 어떻게 동작할 것인지 선택할 수 있습니다. 미리 준비된 Dispatcher 들을 제공하고 있으며 대표적인 예는 다음과 같습니다.

  • Dispatchers.Default: CPU 사용량이 많아서 별도 thread로 분리해야 하는 경우입니다.
  • Dispatchers.IO: Network이나 File 등을 처리할 때 사용합니다.
  • Dispatchers.Main: 기본적으로 사용하지 못하는데, 안드로이드의 경우는 사용 가능합니다. Main UI Thread 에서 동작하도록 할 수 있습니다.


코루틴의 사용 방법

  1. 어떤 Dispatcher를 사용할 것인지 정합니다.
  2. 위에서 정한 Dispatcher를 이용해서 CoroutineScope를 생성합니다.
  3. Coroutine의 launchasync에 동작할 코드 블럭을 전달합니다. launchJob, asyncDeferred 객체를 리턴합니다.
  4. 위에서 리턴한 Job이나 Deffered 객체를 제어합니다. cancel, join 등으로 코루틴을 제어할 수 있습니다.

let 명령어에 else 처리하기

|

예제 코드

fun main(args: Array<String>) {
    var str1: String? = "hello"
    var str2: String? = null

    str1?.let {
        println("str1(1): $str1")
    } ?: run {
        println("str1(2): $str1")
    }

    str2?.let {
        println("str2(1): $str2")
    } ?: run {
        println("str2(2): $str2")
    }
}

결과값은 다음과 같습니다.

str1(1): hello
str2(2): null

코루틴(Coroutine) Pause/Resume 사용하기

|

SAM 변환

import android.os.Bundle
import android.os.Handler
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume

class MainActivity : AppCompatActivity() {

    private var continuation: Continuation<String>? = null

    private var count = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.i("snowdeer", "[snowdeer] onCreate()")

        run_button.setOnClickListener {
            startCoroutine()
        }
        next_button.setOnClickListener {
            continuation?.resume("Resumed")
        }

    }

    private fun startThread() {

    }

    private fun startCoroutine() {
        Log.i("snowdeer", "[snowdeer] startCoroutine()")

        GlobalScope.launch {
            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()
        }

        Log.i("snowdeer", "[snowdeer] end of startCoroutine()")
    }

    private fun refreshCount() {
        count++
        count_textview.text = "$count"
    }

    private suspend fun pause() = suspendCancellableCoroutine<String> {
        continuation = it
    }
}