0. 코루틴이란?

  • 코루틴은 비동기 실행을 간단하게 하기위한 concurrency design pattern이다.
    • suspendable computation 기법을 사용한다.
      • 함수의 실행을 특정 지점에서 일시 중단하고 나중에 다시 실행할 수 있는 기법
  • 코루틴(Coroutine)은 코틀린(Kotlin)에만 존재하는 것이 아니다.
    • Python, Go, C#, Js 등에서도 사용하는 개념이다.

1. Main Routine, Sub Routine, Co Routine

Subroutine vs Coroutine

  • 일반적으로 한 함수에서 다른 함수를 실행할 때, 호출되는 함수를 서브루틴이라고 부른다.
fun register() {
	...
	getDeviceToken()
	...
}

fun getDeviceToken() {
	...
}
  • getDeviceToken() 함수는 register() 함수의 서브루틴이다.

 

  • 코루틴은 호출한 함수의 안에서 수행되는 것이 아니라 호출한 함수와 함께 수행되는 루틴이다.

  • coroutine1안에서 coroutine2가 실행되는 것이 아니다.
  • 각 코루틴은 하나의 routine이며 쓰레드를 공유하며 함께 실행되는 것이다.
    • 따라서 병렬적으로 처리하는 것이 아닌 동시적으로 처리하는 기법이다.

Concurrency vs Parallelism

  • CPU의 코어는 1개의 프로세스만 실행할 수 있다.
  • 싱글 코어 CPU에서는 멀티태스킹을 Concurrent하게 처리한다.
    • 실제로 프로세스가 동시에 실행되지 않는다.
    • 시분할을 통해 동시에 실행되는 것 처럼 동작한다.
 

  • 멀티태스킹을 Parallel하게 하려면 코어나 CPU가 여러개 있어야한다.
    • 같은 시간에 여러 프로세스가 실행된다.

2. 동작방식 - H/W

Thread

  • 쓰레드는 같은 프로세스 내에서 Heap, Code, Data를 공유하며, Stack영역을 독립적으로 가진다.
    • Thread단위로 작업하면 context switching 비용이 프로세스에 비해 적어진다.

Coroutine

  • 코루틴의 작업 단위는 Coroutine Object로 JVM의 Heap에 적재된다.
  • 한 쓰레드에서 Coroutine Object만 변경되기 때문에 context switching이 필요없다.
    • 프로그래밍적으로 switching을 할 수 있다.
    • Coroutine을 Lightweight Thread라고도 한다.

3. 사용

코루틴은 크게 3가지로 구성된다.

  • Coroutine Scope
  • Coroutine Context
  • Coroutine Builder

코루틴이 어떤 범위에서 동작하는 지에 대한 Scope를 정하고, 어떤 쓰레드에서 코루틴을 실행할 지 Context를 설정하고, 코루틴 객체를 만드는 Builder로 구성된다.

Scope

  • CoroutineScope
    • 코루틴이 필요할 때 만들고, 필요하지 않을 때 취소할 수 있는 Scope이다.
    • 안드로이드에서는 라이프사이클을 따라간다.
  • GlobalScope
    • Application이 종료될때 까지 유지되는 Scope이다.
    • Singleton으로 만들어져 있다.
    • 메모리 누수가 생기기 쉽다.

Context

Dispatchers

코루틴이 실행이되는 쓰레드를 지정

  • Default
    • 많은 CPU 연산이 필요한 작업을 하기위한 백그라운드 쓰레드
  • IO
    • File, Network, DB IO를 위한 쓰레드
  • Main
    • UI 쓰레드

Builder

launch

  • Job 객체 반환
val job: Job = CoroutineScope(Dispatchers.IO).launch {
 // 비동기함수()
}

async

  • Deferred 객체 반환
    • 람다 안에 마지막 줄이 반환된다.
    • await() 메소드가 호출되면 async가 실행되고 실행을 완료하고 결과를 반환할 때 까지 기다린다.
suspend fun doWorld() {  // this: CoroutineScope
  val deferredInt: Deferred<Int> = CoroutineScope(Dispatchers.IO).async {
		println(1)
		2
	}
  println(3)

	val result: Int = deferredInt.await()
  
  println("$result")
  println(4)
}

// 결과
3
1
2
4

runBlocking

  • 현재 쓰레드를 block하고, 코루틴 함수를 실행한다.

withContext

  • Dispatcher 변환
    • Main 스레드에서 작업하는 중에 DB작업 같은 IO작업이 필요할때 사용할 수 있다.
CoroutineScope(Dispatchers.Main).launch {
	updateUI1()
	updateUI2()
	withContext(Dispatchers.IO) {
		insertUserData()
	}
	updateUI3()
}

Job & Deferred

  • 코틀린에서는 Coroutine Object를 Job이나 Deferred로 정의한다.
    • Deferred는 결과값을 가지는 Job이다.
  • 코루틴의 상태를 가지고 있고 흐름을 관리할 수 있다.
    • start()
      • 중단 없이 실행됨
      • 실행을 기다리지 않는다. 호출한 쓰레드가 종료되면 같이 종료
    • join()
      • Job의 동작이 완료될 때 까지 코루틴 일시 중단
    • cancel()
      • 코루틴을 종료하도록 유도한다. 대기하지 않는다.
    • cancelAndJoin()
      • 코루틴 종료 신호를 보내고 종료할 때 까지 기다린다.
    • cancelChildren()
      • 자식 코루틴 종료
  • Job states cycle
                                          wait children
    +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
    | New | -----> | Active | ---------> | Completing  | -------> | Completed |
    +-----+        +--------+            +-------------+          +-----------+
                     |  cancel / fail       |
                     |     +----------------+
                     |     |
                     V     V
                 +------------+                           finish  +-----------+
                 | Cancelling | --------------------------------> | Cancelled |
                 +------------+                                   +-----------+

예외처리

  • CoroutineExceptionHandler를 사용하여 예외처리를 할 수 있다.
    • 코루틴 안에서 예외 발생 시 exception을 handler로 전달하여 처리하는 흐름을 만들 수 있다.
    • launch vs async
val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
    throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)

4. 예시

  • 앱에 로그인 했을 때, Sendbird User를 만들고 디바이스 토큰을 등록하는 상황
    • 유저등록이 완료된 후, 디바이스 토큰을 등록해야 한다.
  • callback
fun initializeSendbird() {
	createUser() {
		registerToken() {
			doSomething() 
		} 
	}
}
  • rx
fun initializeSendbird() {
	Observable
		.just(...)
		.observeOn(MAIN_Thread)
		.subscribeOn(IO_Thread)
		.flatMap { () -> createUser() }
		.flatMap { () -> registerToken() }
		.subscribe( { () -> doSomthing() }, { fail() })
}
  •  coroutine
    • 비동기 작업을 순차적으로 작성할 수 있다.
      • initializeSendbird() 함수가 코루틴이기 때문에, createUser()를 호출하고, 코루틴을 빠져나간다. (다른 스레드에서 실행)
      • createUser()가 완료되면 코루틴으로 들어와서 다음 함수를 호출한다.
suspend fun initializeSendbird() {
    val result = createUser()
    registerToken(result)
    doSomething()
}

 

5. 동작방식 - S/W

  • 코루틴은 어떻게 suspendable하게 동작하는가?
  • 코루틴은 CPS(Conrinuation Passing Style)이라는 형태의 코드로 변환된다.
    • label로 중단점을 나누고, 함수 실행 완료되면, resume을 호출하여 코루틴을 재개한다.
suspend fun myCoroutine(cont: MyContinuation) {
    val userData = fetchUserData()
    val userCache = cacheUserData(userData)
  updateTextView(userCache)
}

⬇️

fun myCoroutine(cont: MyContinuation) {
    when(cont.label) {
        0 -> {
            cont.label = 1
            fetchUserData(cont)
        }
        1 -> {
            val userData = cont.result
            cont.label = 2
            cacheUserData(userData, cont)
        }
        2 -> {
            val userCache = cont.result
            updateTextView(userCache)
        }
    }
}

fun fetchUserData(cont: MyContinuation) {
    val result = "[서버에서 받은 사용자 정보]"
    cont.resumeWith(Result.success(result))
}

출처

[Coroutine] Coroutine(코루틴)과 Subroutine(서브루틴)의 차이 - Coroutine이란 무슨 뜻일까?

Coroutine, Thread 와의 차이와 그 특징

알기쉬운 코루틴 이론::Android Studio에서 Kotlin으로#28

코틀린 코루틴(coroutine) 개념 익히기 · 쾌락코딩

Coroutine exceptions handling | Kotlin

[Kotlin] Coroutine - 코루틴의 내부 구현

+ Recent posts