[Kotlin] How coroutine works

Chloe Choi·2021년 3월 29일
0

Kotlin

목록 보기
6/11

배경

개발자들은 항상 앱이 block되는 것에 대해 고민해왔다!

네트워킹 요청 등 long running task를 메인 스레드에서 처리하면 앱이 느리게 느껴지거나 ANR이 발생한다.

이런 상황을 예방하기위해 다음 비동기 프로그래밍 방식을 수십년 간 고안해왔다.

Threading

long running process를 분리된 스레드에서 진행하는 방법이다. 이 방법은 다음 drawback들이 존재한다.

  • 비싼 context switching 비용
  • 한정적인 thread 수
  • 디버깅 등의 어려움

Callback

함수를 매개변수로 전달하고, 프로세스가 종료되면 그 함수를 invoke하는 방식이다. 이 방법 또한 다음 이슈들이 존재한다.

  • nested callbacks a.k.a. 콜백지옥. 대표적인 안티 패턴이다!
  • 어려운 에러 핸들링 및 전파

위 비동기 프로그래밍 방법을 기반으로 한 thin language feature인 코루틴이 고안되었다. 이 코루틴에 대해 자세히 알아보겠다!

Coroutine

이걸 알고싶어서 일주일간 틈틈이 알아봤다. 근데 다들 효과만 설명하고 ㅜㅜ 답답한 부분이 너무 많았다.. 그러다 찾은 2018 드로이드나이츠 강연..! 도창욱님 께서 아주 답답했던 부분을 잘 알려주셨다..! 감사합니다 🙇🏻‍♀️

Goal

Blocking code를 처리하기 위해 Thread를 많이 사용한다. 하지만, 위에서 언급한 것과 같이 Thread는 다음 단점들이 있다.

  • 한정적인 thread 수
  • 비싼 context switching 비용

게다가, Thread 안에서도 Blocking이 일어날 수 있다!

Blocking?
어떤 일을 기다리며 그 흐름이 block 되는 상황
ex. 한 스레드 내에서 네트워크 요청을 보내면 그 스레드는 응답이 돌아올 때 까지 block 되어있다.

Blocking 문제를 어떻게 해결할 수 있을까?

일단 Subroutine을 알아보자

단일 시작지점과 종료지점을 갖는다. 시작과 종료 사이에 blocking이 발생하는 것이다! 이 blocking을 없애기 위해서는 어떻게 해야할까?


이렇게 중간에 나갔다가 다시 들어온다면?? blocking 시점에서 멈춤(suspend)했다가 해당 지점에서 재개(resume)하자!
이렇게 routine끼리 협력한다는 의미에서 co-routine이라는 이름을 갖게되었다

How it works

우선, 작동원리를 알아볼 예제 코드이다.

suspend fun fetchUserDetail(id: String) {
    val token = auth()
    val user = getUser(token, id)
    
    updateUserData(user)
}
// 단, auth와 getUser는 suspend 키워드가 붙은 함수이다.

위 goal을 적용하면 이 함수는 다음과 같이 진행되어야 한다.

자 이제 어떻게 suspend되고 resume되는지 진짜 알아보자.

함수가 실행될 때 다음 상태들을 갖고있는다.

  • 누가 호출했는지
  • 어디까지 샐행했는지 (@PC)
  • 어떤 값들을 가지고 있는지

그럼 위 상태들을 저장하면 다시 시작할 수 있지 않을까? ContextSwitching이 일어날 때 PCB, TCB에 저장하는 것처럼 말이다!

상태를 저장하고 전달하는 것을 Continuation Passing Style이라고 한다.

Continuation란 뭘까? 코루틴에서는 PCB, TCB와 같은 역할을 하는 것이다.

public interface Continuation<in T> {
   public val context: ConrotineContext
   
   public fun resume(value: T)
   public fun resumeWithException(exception: Throwable)
}

위 fetchUserDetail 함수가 컴파일 되면 어떻게 되는지 간단히 확인해보자

fun fetchUserDetail(id: String) {
    // ..
    switch (label) {
    case 0: val token = auth()
    case 1: val user = getUser(token, id)
    case 2: updateUserData(user)
    }
}

suspend 키워드가 없어지고 switch문이 추가되었다. label 값에 따라 실행 위치가 달라진다! 저 label 값을 Countinuation으로 관리하는 것이다. 상태를 관리하는 부분 코드까지 같이 확인해보자

fun fetchUserDetail(id: String, cont: Continuation) {
    val sm = object: CoroutineImpl { ... }
    switch (label) {
    case 0:
        sm.id = id
        sm.label = 1 // 다음 실행될 부분 지정 ❗️❗️❗️
        val token = auth()
    case 1:
        val id = sm.id
        val token = sm.result as String
        sm.label = 2
        val user = getUser(token, id)
    case 2: updateUserData(user)
    }
}
  • StateMachine에 의해 코드의 실행 위치 처리를 한다
  • 함수에 대한 진입/진출 시 상태를 저장/복원하기 위한 처리가 있다

이렇게 CPS transform을 통해 suspend, resume하는 것을 확인할 수 있다!

따라서, Sequencial code를 통해 non-blocking 코드를 작성할 수 있다❗️❗️

// 콜백 사용 시
inChannel.read(buf) {
	process(buf, bytesRead)
    // ...
    
    outChannel.write(buf) {
    // ...
    outFile.close()
    }
}
// 👉 sequencial 하지 않음

// 코루틴 사용 시
launch(CommonPool) { // 코루틴 빌더로 시작
	while (true) {
    	val bytesRead = inFile.aRead(buf) // 여기서 멈추고 코드를 나가버림 => 다른 게 thread를 사용할 수 있도록 하고 나중에 이 지점에서 resume
        if (bytesRead == -1) break
        // ...
        process(buf, bytesRead)
        outFile.aWrite(buf)
        // ...
    }
    // sequencial 하게 작성된 코드
}
profile
똑딱똑딱

0개의 댓글