Coroutine

코루틴이란. 실행의 지연과 재개를 허용함으로서, 비선점 멀티테스킹을 위한 서브루틴을 일반화한 구성요소입니다.
자세히 이해하기 위해 서브루틴비선점 멀티테스킹 두가지 개념을 알아야 합니다.

 

루틴과 서브루틴

루틴은 컴퓨터 프로그램에서 하나의 정리된 일(job)입니다.
프로그램은 보통 크고 작은 여러가지 루틴을 조합시킴으로써 만들어집니다.

루틴은 다시 메인루틴서브루틴으로 나뉩니다.
메인루틴은 프로그램 전체의 개괄적인 동작 절차를 표시하도록 만들어 집니다.
서브루틴은 반복되는 특정 기능을 모아 별도로 묶어 이름을 붙여 놓은 것입니다.
그리고 별도의 메모리에 해당 기능을 모아놓고 서브루틴이 호출될 때마다 저장된 메모리로 이동했다가 return 을 통해 원래 호출자의 위치로 돌아오게 됩니다. (function과 비슷한 개념)

 

서브루틴과 코루틴 (subroutine and coroutine)

코루틴도 루틴의 일종입니다. 다만 세가지의 차이점이 있습니다.

  • 코루틴에서는 메인-서브 개념이 없습니다. 모든 루틴들이 서로를 호출할 수 있습니다.
  • 서브루틴의 경우 메인루틴에서 특정 공간으로 이동한 후 return에 의해 돌아와 다시 프로세스를 진행하는데 반해,
    코루틴의 경우 루틴을 진행하는 중간에 멈추어서 특정 위치로 갔다가 다시 원래 위치로 돌아와 나머지 루틴을 실행할 수 있습니다.
  • 서브루틴은 진입점과 반환점이 단 하나밖에 없어 메인루틴에 종속적입니다.
    하지만 코루틴은 진입지점이 여러개이기 때문에 메인루틴에 종속적이지 않고 대등하게 데이터를 주고 받을 수 있습니다.

 

비선점형 멀티태스킹과 선점형 멀티태스킹

하나의 Task가 Scheduler로 부터 자원을 할당 받았을 때,
Scheduler가 자원을 강제로 뺐을 수 있으면 선점형 멀티테스킹입니다.
반대로 뺐을 수 없으면 비선점형 멀티테스킹입니다.

스레드는 선점형 멀티테크킹이지만, 코루틴은 비선점형 멀티테스킹입니다. 
즉, 코루팅은 병행성(Concurrency)을 제공하지만 병렬성(Parallelism)을 제공하지 않습니다.

이어서 병행성과 병렬성의 차이점을 알아보겠습니다.

병행성 (Concurrency)

  • 동시에 실행되는 것처럼 보입니다.
  • Logical Level에 속합니다.
  • Single Core를 사용합니다.
  • 물리적으로 병렬이 아닌 순차적으로 동작할 수 있습니다.
  • 실제로는 Time-sharing으로 CPU 자원을 나눠 사용함으로써 사용자가 Concurrency를 느낄 수 있도록합니다.

병렬성 (Parallelism)

  • 실제로 동시에 작업이 처리됩니다.
  • Physical(Machine) Level에 속합니다.
  • 오직 Multi Core에서만 가능합니다.

다시 돌아와서 선점형 멀티테스킹인 스레드에 비해 비선점형 멀티테스킹인 코루틴의 장점은 아래와 같습니다.

  • 스레드간 작업 교환은 system call 또는 blocking call 콜 등의 비용이 발생하지만 코루틴간 작업 교환 비용이 적습니다.
  • 동기화 작업을 위한 mutexes, semaphores 등의 장치가 필요없습니다.
  • Single Core이기 때문에 OS의 지원이 필요 없습니다.

결론적으로 코루틴은 스레드에 비해 비용이 적은 멀티테스킹 방식입니다.

 

코틀린 코루틴

2018년 10월 29일 Kotlin 1.3에 코틀린 코루틴이 추가되어 코루틴을 사용할 수 있게 되었습니다.

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

// 출력 결과
// Hello,
// World!

위 예제에서 GlobalScope는 전체 어플리케이션의 라이프타임입니다.
launch는 해당 코드 블럭에서 현재 스레드를 blocking 하지 않고 코르틴으로 실행한다는 의미입니다.

위의 예제에서 delay 함수는 스레드를 blocking 할 수 없는 suspending 함수입니다.
실제 스레드를 blocking 하기 위해서는 runBlocking을 사용하여 아래와 같이 작성하면 되겠습니다.

internal class MainTest {
  @Test
  fun main() {
    GlobalScope.launch {
      delay(1000L)
      println("World!")
    }
    println("Hello")
    runBlocking { delay(2000L) }
  }
}

coroutineScope 는 runBlocking 과 다르게, 모든 자식들이 완료될 때까지 현재 스레드를 블락시키지 않습니다.
해당 coroutineScope를 함수로 만들 수 있는 키워드가 suspend 입니다.

 

Suspend

Suspend는 '중지하다' 라는 뜻입니다.
코루틴에서 suspend 키워드는 아래와 같이 정의되어 있습니다.

a function that could be started, paused, and resume.
시작하고, 멈추고, 다시 시작할 수 있는 함수
internal class MainTest {
  suspend fun doSomethingUsefulOne(): Int {
    delay(ofSeconds(1))
    return 13
  }

  suspend fun doSomethingUsefulTwo(): Int {
    delay(ofSeconds(1))
    return 29
  }

  @Test
  fun main() {
    runBlocking {
      val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
      }
      println("Completed in $time ms")
    }
  }
}

// 출력 결과
// The answer is 42
// Completed in 2029 ms

동시 처리를 원한다면 아래와 같이 async 를 사용하면 됩니다.

internal class MainTest {
  suspend fun doSomethingUsefulOne(): Int {
    delay(ofSeconds(1))
    return 13
  }

  suspend fun doSomethingUsefulTwo(): Int {
    delay(ofSeconds(1))
    return 29
  }

  @Test
  fun main() {
    runBlocking {
      val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
      }
      println("Completed in $time ms")
    }
  }
}

// 출력 결과
// The answer is 42
// Completed in 1031 ms

async scope 함수는 아래와 같은 특징을 가지고 있습니다.

  • 동시에 수행할 수 있습니다.
  • Deferred<T> Type을 리턴합니다. (js에서 Promise와 같은 형식)
  • 같은 scope 내에 하나라도 처리가 실패하면 모든 자식들이 다 처리 실패됩니다. (throw exception)

 

job.join()

internal class MainTest {
  suspend fun doSomethingUsefulOne(): Int {
    delay(ofSeconds(1))
    return 13
  }

  suspend fun doSomethingUsefulTwo(): Int {
    delay(ofSeconds(1))
    return 29
  }

  @Test
  fun main() {
    runBlocking {
      val job = launch {
        repeat(10) { i ->
          println("job: I'm sleeping $i ...")
          delay(ofMillis(500))
        }
      }
      delay(ofSeconds(2))
      println("main: I'm tired of waiting!")
      job.join() // job 이 완료될 때까지 기다림
      println("main: Now I can quit.")
    }
  }
}

// 출력 결과
// job: I'm sleeping 0 ...
// job: I'm sleeping 1 ...
// job: I'm sleeping 2 ...
// job: I'm sleeping 3 ...
// main: I'm tired of waiting!
// job: I'm sleeping 4 ...
// job: I'm sleeping 5 ...
// job: I'm sleeping 6 ...
// job: I'm sleeping 7 ...
// job: I'm sleeping 8 ...
// job: I'm sleeping 9 ...
// main: Now I can quit.

 

job.cancel()

internal class MainTest {
  suspend fun doSomethingUsefulOne(): Int {
    delay(ofSeconds(1))
    return 13
  }

  suspend fun doSomethingUsefulTwo(): Int {
    delay(ofSeconds(1))
    return 29
  }

  @Test
  fun main() {
    runBlocking {
      val job = launch {
        repeat(10) { i ->
          println("job: I'm sleeping $i ...")
          delay(ofMillis(500))
        }
      }
      delay(ofSeconds(2))
      println("main: I will cancel job!")
      job.cancel() // job 을 취소함
      println("main: Now I can process.")
    }
  }
}

// 출력 결과
// job: I'm sleeping 0 ...
// job: I'm sleeping 1 ...
// job: I'm sleeping 2 ...
// job: I'm sleeping 3 ...
// main: I will cancel job!
// main: Now I can process.

 

Dispatchers

코루틴 실행에 사용하는 스레드를 결정합니다.
모든 코루핀 빌더 (ex. launch, async 등)는 디스패처를 지정할 수 있습니다.
다음은 대표적인 Dispathers 입니다.

  • Default: 오래 걸리는 작업을 할 때, 공유된 백그라운드 스레드 풀을 사용합니다.
  • IO: 파일을 쓰거나 API 콜 같은 상황에서 사용합니다.
  • Main: 메인 스레드 작업에 사용합니다.
@Test
fun main() {
  runBlocking(Dispatchers.IO) {
    // do something
  }
}