Coroutine Intro

#Kotlin #Kotlin_Coroutine

suspend.png

코루틴을 어떻게 이해할 수 있을까?

코루틴은 너무나 강력하고 복잡한 구성과 구현으로 만들어져있지만, 코루틴 자체가 무엇인지는 매우 단순하다. 코루틴을 잘 사용하려면 코루틴 자체가 무엇인지만 알면 되었다. (하지만 코루틴 자체가 무엇인지도 여러 각도에서 봐야하고 그렇게되면 코루틴을 정의할 수 없어 보일만큼 다르게 보이기도 하는데, 그 모습들을 연결하다보면 코루틴 하나의 모습이라는걸 알게된다.)

여러 각도에서 코루틴의 정의를 살펴보자. (이 모습들을 모두 통합적으로 생각해주길.)


1. Coroutine is just a suspendable computation!

coroutine is an instance of a suspendable computation.

Kotlin Docs, Coroutine basics

이 포스트의 배너로 해 둘만큼 'Suspendable'이란 의미는 코루틴에게 전부라고 생각한다.
Suspendable은 '중단(연기)될 수 있는'이라는 뜻으로 코루틴은 중단될 수 있는 연산 루틴을 의미한다.

중단되면 어떻게되는데? 중단되는게 좋은거야? 루틴이 뭔데? 에 대해서 궁금증을 가져보자


2. Coroutine is sort of routine!

2-1. Routine? - Sequence of actions

아래의 아주 간단한 코드를 보자

var count = 0

for(i in 1..10) {
	count += i
}

println("count: $count")

if (count == 55) {
	println("Bravo!")
}
  1. count라는 변수를 생성하고 0을 할당한다.
  2. 반복문을 실행한다. (1부터 10까지 count에 차례대로 더한다.)
  3. count: <count 변수 값>를 출력한다.
  4. count가 55이면 Bravo!를 출력하고 count가 55가 아니라면 출력하지 않는다.

이렇게 일련의 작업들, 순서를 이루어 진행되는 일들을 루틴이라고 한다.
우리가 일상에서 루틴을 만들었다는 말을 아침에 일어나자마자 뭘하고 몇시에 출근해서 뭘 타고 회사에 도착하면...등등의 과정을 루틴이라고 하는 것처럼.

coroutine도 이런 routine의 의미에 co- 접두사를 붙인 것이다. 한편 subroutine이라는 것도 있는데, 이것도 routine의 의미에 sub- 접두사를 붙인 것이다. routine과 이 둘은 어떤 관계가 있고, 서로는 어떻게 다를까?

2-2. Subroutine?

함수라고 생각해도 무방할 것 같다. 서브루틴은 진입점과 탈출점이 하나씩인 루틴을 의미하니 말이다.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    doSubroutine() // [doSubroutine()] result= 12345678910ABCDEFhelloworld
    // println("")
    // doCoroutine()
}

fun doSubroutine() {
           
   print("[doSubroutine()] result= ")
   
   plus1To10()
   plusAToF()
   plusHelloWorld() 
}

fun plus1To10() {
    for (i in 1..10) {
        print("$i")
    }
}

fun plusAToF() {
	val aToF = "ABCDEF"
    
    aToF.forEach {
        print(it)
    }
}

fun plusHelloWorld() {
	val helloWorld = "helloworld"
    
    helloWorld.forEach {
        print(it)
    }
}

라고 했을때 이 plus1To10(), plusAToF(), plusHelloWorld() 함수는 doSubroutine() 함수에 의해서 호출되는 서브함수들이다. 이것을 스택으로 표현해보면 아래와 같다.
function_stack.png

이 스택을 설명해보면,

중요한 것은 plus1To10(), plusAToF(), plusHelloWorld() 함수가 실행될 수 있기 위해서는 doSubroutine() 함수에서 이들을 call 해야 한다(진입점이 하나). 그리고 함수가 종료되는 지점은 각 함수의 작업을 모두 마쳤을 때 이다(탈출점이 하나).

doSubroutine()이 메인 루틴이라고 한다면, doSubroutine() 입장에서 plus1To10(), plusAToF(), plusHelloWorld()는 서브루틴이라고 할 수 있다. 이렇게 서브루틴은 다른 루틴과 master-slave 관계를 가진다고 설명할 수도 있다.

2-3. Coroutine?

Co- 접두사는 '함께하는'의 의미이다. 'co-worker', 'co-founder'를 봤을 때 같이 일하는 사람, 같이 사업을 시작한 사람 인 것 처럼. 그럼 Coroutine은 '같이 실행되는 루틴' 정도가 될테고, 이걸 조금 더 루틴과 연관지어 설명하면 같이 실행되는 다른 코루틴의 영향을 받아 진입점(정확히 말하면 재진입점)과 탈출점이 여러개인 루틴을 의미한다. (서브루틴도 코루틴의 일종으로 볼 수 있지만, 여기에서는 서브루틴과 다른 점을 살펴본다.)

아래의 코드를 보자.

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    // doSubroutine() // [doSubroutine()] result= 12345678910ABCDEFhelloworld
    // println("")
    doCoroutine() //[doCoroutine()] result= 1Ah2Be3Cl4Dl5Eo6Fw7o8r9l10d
}

fun CoroutineScope.doCoroutine() {
       
   print("[doCoroutine()] result= ")
   
   launch {
      suspendPlus1To10() 
   }
   
   launch {
      suspendPlusAToF() 
   }
   
   launch {
      suspendPlusHelloWorld() 
   } 
}

suspend fun suspendPlus1To10() {
    for (i in 1..10) {
        print("$i")
        yield()
    }
}

suspend fun suspendPlusAToF() {
	val aToF = "ABCDEF"
    
    aToF.forEach {
        print(it)
        yield()
    }
}

suspend fun suspendPlusHelloWorld() {
	val helloWorld = "helloworld"
    
    helloWorld.forEach {
        print(it)
        yield()
    }
}

테스트의 결과를 보자. "1Ah2Be3Cl4Dl5Eo6Fw7o8r9l10d" 라는 값을 볼 수 있다. 이것을 해석하면, suspendPlus1To10()를 실행했다가 이 함수(a.정확하게 말하면 이 함수를 실행하고 있는 코루틴이다.)를 잠시 멈추고(탈출점) suspendPlusAToF()를 실행했다가 이것도 멈추고, suspendPlusHelloWorld()를 실행했다가 이것도 멈추고, 다시 suspendPlus1To10()를 실행한다.(재진입점) 이 과정을 계속 반복한다. (탈출점과 재진입점이 여러개이다.)

서브루틴과 달리 코루틴은 Concurrency하게 실행이 가능하다. Concurrency를 이해하기 위해서는 Parallelism과의 차이점을 알아야 하는데, 이 과정에서 얘기가 복잡해지므로 지금은 '동시에 실행되는 것 처럼 보이게 하는'이라는 정도로만 알아두자.
coroutine_concurrency.png|300
coroutine_concurrency_flow.png|250

이제 루틴에 대해서 알아봤으니 다시 '중단'으로 돌아가자


1. suspend fun() suspends coroutine which is suspendable!

코루틴에 대해 개념을 잘 잡지 못하는 이유는 '누가 누구를 중단시키는가?', '중단으로 무엇이 달라지는가?' 라는 두 문장에서 '누가'와 '누구' 그리고 '무엇' 에 대해 제대로 이해하고 있지 못하기 때문이라는 생각이 들었다.

처음 코루틴을 접할때는 이 누구와 누가에 대해 큰 신경을 쓰지 않고 나머지 부분을 이해하려고 했었다. 그렇다보니 나의 이해에서 항상 한 조각이 빠져있다는 느낌이 들었고 매번 코루틴 개념을 떠올릴 때마다 헷갈리게 되었다.

1-1. 누가 누구를 중단 시키는가?

'Suspend'를 네이버 사전에서 찾아보면 '유예하다, 중단하다'라고 되어있다. 이건 분명 자동사의 뜻이다. 그.러.나. 그 위에 쓰여진 형태는 타동사라고 되어있다 (아마 거의 대부분의 경우 be+p.p로 사용되기 때문에 뜻을 이렇게 써준거 아닐까). 즉, suspend 자체는 '중단 하는' 것이 아니라 '중단 시키는' 의 의미를 가지고 있다고 봐야한다. 그러므로 suspend fun는 중단 하는 함수가 아니라 중단 시키는 함수일 것이다. 그럼 여기서 무엇을? 이라는 목적어를 채워넣어야 한다. 그 목적어는 2.의 a첨자에서도 언급했듯이 '코루틴'을 중단시키는 것이다.

delay is a special suspending function. It suspends the coroutine for a specific time.

Kotlin Docs, Coroutine basics

실제로 Kotlin Docs에 나와있는 suspending function에 대한 설명에서도 코루틴을 중단시킨다고 나와있다. 그러므로 앞으로는 suspend fun를 중단 되는 함수가 아닌 '자신이 속한 코루틴을 중단 시킬 수 있는' 혹은 '자신이 속한 코루틴의 중단점이 될 수 있는' 함수라고 생각하자.

1-2. 중단으로 무엇이 달라지는가?

스레드의 사용 가능 상태가 달라진다.
중단된 코루틴을 실행시키고 있었던 스레드에서 중단된 코루틴이 사라지면서 해당 스레드는 다른 코루틴을 실행시킬 수 있게된다.

Suspending a coroutine does not block the underlying thread, but allows other coroutines to run and use the underlying thread for their code.

Kotlin Docs, Coroutine basics

한편 이것은 Concurrency와 연결된다. Concurrency는 하나의 스레드에서 여러개의 task를 여러 조각으로 나누어 번갈아가며 실행하여 동시에 실행되는 것과 같은 효과를 내는 것이기 때문이다. 코루틴은 스스로를 중단시키고 재개시킬 수 있기 때문에(non pre-emptive이면서 재진입점과 탈출점을 여러개 가지는 루틴) 코루틴 자체로 Concurrency의 달성이 가능하다. (node.js 같은 경우의 async/await의 경우에는 이벤트루프라는 장치가 있어 Concurrency의 달성이 가능하다.)


3. Coroutine is routine, all routines have its own Scope!

Scope는 코루틴이다. 코루틴은 Scope이다.
라고 설명해도 되지 않을까?

'코루틴 안에 자식 코루틴, 자식 코루틴 하나 더, 자식 코루틴 안에 그 자식 코루틴.'을 만들 수 있는 이유도 '최외각 스코프 안에 스코프 두개, 그 중 하나의 스코프 안에 하나의 스코프'를 생성할 수 있기 때문이다. 이 Scope는 중단되는 범위의 단위가 되며, 스코프를 구조화할 수 있기 때문에 코루틴의 Structured Concurrency가 가능해진다.

interface CoroutineScope

Defines a scope for new coroutines. Every coroutine builder (like launchasync, etc.) is an extension on CoroutineScope and inherits its coroutineContext to automatically propagate all its elements and cancellation.

The best ways to obtain a standalone instance of the scope are CoroutineScope() and MainScope() factory functions, taking care to cancel these coroutine scopes when they are no longer needed (see section on custom usage below for explanation and example).

Kotlin api - CoroutineScope

3-1. CoroutineScope의 구조

CoroutineScope는 CoroutineContext를 가진다. CoroutineContext의 구현체는 Job과 Dispatcher가 있는데, 이것은 각각 실행 Task와 코루틴을 thread에 할당해주는 객체를 의미한다. 지금은 CoroutineScope가 최상위이고, CoroutineScope가 CoroutineContext라는 컨텍스트 객체를 가지고 있으며, 컨텍스트에는 Job이라는 것과 Dispatcher라는 것이 있다. 정도만 기억하자.
coroutinescope_dml.png

Kotlin API - CoroutineDispatcher
Kotlin API - Job


4. 참고