반갑습니다!

[Kotlin] Principle of Coroutine 본문

Kotlin

[Kotlin] Principle of Coroutine

김덜덜이 2020. 7. 5. 02:18

코루틴이 쓰레드를 대체할 수 있다는 것은 앞의 예제들을 학습하면서 알 수 있었다. 이번에는 코루틴이 실제로 어떻게 동작하는 것인지 알아보도록 하자.

CPS (Continuation Passing Style)

코루틴은 컴파일러에 의해서 CPS (Continuation Passing Style)로 변환된다.

fun postItem(item: Item) {
      val token = requestToken()
      val post = createPost(token, item)
      processPost(post)
}

이런 형태의 코드가 있다고 하자. 이 코드는 내부적으로 다음과 같은 코드로 변환된다.

fun postItem(item: Item) {
	requestToken { token ->
    	val post = createPost(token, item)  	// Continuation
        processPost(post)						// Continuation
    }
}

CPS는 쉽게 말해서 콜백과 비슷한 형태라고 이해하면 된다.

How does it works?

그렇다면 순차적으로 작성한 코드가 어떻게 비동기적으로 동작하고, 중단했다가 다시 실행하는 등의 동작을 할 수 있는 것일까?

suspend fun createPost(token: Token, item: Item): Post { ... }

위와 같이 작성한 코틀린 코드는 역컴파일하게되면 아래와 같은 코드로 변환된다.

Object createPost(Token token, Item item, Continuation<Post>     cont) { ... }

역컴파일되면 Continuation 객체가 매개변수로 추가되면서 CPS로 변환된다.

Labels

suspend fun postItem(item: Item) {
	// LABEL 0
    val token = requestToken()
    // LABEL 1
    val post = createPost(token, item)
    // LABEL 2
    processPost(post)
}

suspend 함수는 컴파일되면서 LABEL이 붙게 되는데, 코루틴은 함수가 제개될 수 있어야하기 때문에 그런 지점을 LABEL을 지정하는 것이다.

suspend fun postItem(item: Item) {
	switch (label) {
    	case 0:
        	val token = requestToken()
        case 1:
        	val post = createPost(token, item)
        case 2:
        	processPost(post)
	}
}

이런식으로 함수를 다시 제개할 수 있도록 변하는 것이다. 그리고 레이블이 완성되면 CPS로 변환되는데 다음과 같은 형태가 된다.

fun postItem(item: Item, cont: Continuation) {
      val sm = object : CoroutineImpl { ... }
      switch (sm.label) {
            case 0:
                  requestToken(sm)
          case 1:
                  createPost(token, item, sm)
          case 2:
                  processPost(post)
    }
}

Continuation 객체는 콜백 인터페이스 같은 것으로 제개해주는 인터페이스를 가진 객체이다. sm은 'State Machine' 을 의미한다. 각각의 함수를 호출할 때는 지금까지 했던 연산의 결과를 넘겨줘야하기 때문에 sm을 매개변수로 넘겨주게 되는데 결국 이 것은 Continuation이고 어떤 정보 값을 가진 형태로 전달되어 코루틴이 동작되는 것이다.

Callback

fun postItem(item: Item, cont: Continuation) {
        val sm = cont as? ThisSM ?: object : ThisSM {
          fun resume(...) {
                postItem(null, this)
        }
    }
      switch (sm.label) {
          case 0:
                  sm.item = item
                  sm.label = 1
                  requestToken(sm)
          case 1:
                  createPost(token, item, sm)
    }
}

각각의 suspend 함수가 sm을 마지막 매개변수로 가져가고 각각의 case를 마치게되면 resume() 을 호출하게 된다. 결국, 각각의 case가 끝날 때마다 label 값을 증가시키고 resume() 을 통해 재귀호출하는 형태로 코루틴이 동작하는 것이다.

Decompile

실제로 코틀린 코드를 역컴파일해서 확인해보자.

Like the dream code

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        val userData = fetchUserData()
        val userCache = cacheUserData(userData)
        updateTextView(userCache)
    }
}

suspend fun fetchUserData() = "user_name"

suspend fun cacheUserData(user: String) = user

fun updateTextView(user: String) = user

위와 같은 코루틴 코드가 있다. 이 코드를 자바 코드로 역컴파일해보면 다음과 같다 .

@Nullable
public static final Object fetchUserData(@NotNull Continuation $completion) { return "user_name"; }

@Nullable
public static final Object cacheUserData(@NotNull String user, @NotNull Continuation $completion) { ... }

@NotNull
public static final String updateTextView(@NotNull String user) {
      Intrinsics.checkParameterIsNotNull(user, "user");
      return user;
}

public final Object invokeSuspend(@NotNull Object $result) {
      Object var10000;
      label17: {
          Object var5 = IntrinsicksKt.getCOROUTINE_SUSPENDED();
          CoroutineScope $this$launch;
          String userData;
          switch(this.label) {
        case 0:
            ResultKt.throwOnFailure($result);
            $this$launch = this.p$;
            this.L$0 = $this$launch;
            this.label = 1;
            var10000 = Example_nomagic_01KT.fetchUserData(this);
            if(var10000 == var5) {
                  return var5;
            }
            break;
        case 1:
            $this$launch = (CoroutineScope)this.L$0;
            ResultKt.throwOnFailure($result);
            var10000 = $result;
            break;
        case 2:
            userData = (String)this.L$1;
            $this$launch = (CoroutineScope)this.L$0;
            ResultKt.throwOnFailure($result);
            var10000 = $result;
            break label17;



                                                 ...




        }
    }
}

실제로도 위에서 설명한 방식처럼 역컴파일 된다는 것을 알 수 있다.

'Kotlin' 카테고리의 다른 글

[Kotlin] Coroutine Context and Dispatchers  (0) 2020.07.05
[Kotlin] Composing Suspending Functions  (0) 2020.07.05
[Kotlin] Cancellation and Timeouts  (0) 2020.07.04
[Kotlin] Coroutine Basics  (0) 2020.07.04
[Kotlin] Coroutine Guide  (0) 2020.07.04