Kotlin

Kotlin Coroutines

dev_roach 2022. 6. 19. 23:20
728x90

Kotlin Coroutines 에 대한 공부를 최근에 많이 하고 있는데 하면서 느껴지는 점은 Thread 간의 Context Switching 에 관한 내용을 Heap 으로 관리할께 라는 느낌이 강하게 들었다. 예전에는 어떻게 Context Switching 을 적은비용으로 한다는 거지 싶었는데, 이제는 대략적으로 이해가 간다. 언제 글을 정리할까 했다가 오늘 천천히 정리해보려고 한다.

기존 Context Switching 의 문제

일단 아주 간단하게 설명하겠다. 어차피 지금 글에서 이 내용이 중요한건 아니니까. 각 Thread 는 Local Stack 등 자신만의 데이터를 가지고 있는데, 다른 Thread 와 Switching 해야 할때 자신의 작업정보를 넘겨주어야 한다. 즉, Process Switching 보다는 싼 비용이지만, 그럼에도 불구하고 Switching 해야 할때 주고 받아야 하는 정보들이 존재한다. 이것이 Context Switching 의 Cost 라고 할 수 있다. 누군가는 아주 당연하게 생각할 수 있다. 당연히 정보를 넘겨 받아야 하는데 당연히 비용이 드는게 아니냐..? 라고 할 수 있다. 하지만 코루틴에서는 조금 다르게 풀었다.

Kotlin Coroutine 의 해결 방안

코틀린 Coroutine 은 Pointer 방식을 해결방안으로 떠올렸다. 즉, Heap 은 Thread 가 공유하고 있는 영역이니까, TCB (Thread Context Block) 에 있는 단위를 Heap 까지 끌어올리고, Thread 가 이 메모리 주소만 참조하게 하면 안될까? 이게 Kotlin Coroutines 에서, Context Switching 비용을 줄인 방법이다. Kotlin 은 이를 위해서 Continuation 이라는 객체를 도입했다.

Continuation

Kotlin 에서 suspend 된 함수들을 보면 컴파일 시 아래와 같이 변경된다.

suspend fun getUser(): User? suspend fun setUser(user: User) suspend fun checkAvailability(flight: Flight): Boolean // under the hood is fun getUser(continuation: Continuation<*>): Any? fun setUser(user: User, continuation: Continuation<*>): Any fun checkAvailability( ​​​flight: Flight, continuation: Continuation<*> ): Any

보면 Continuation 이라는 Type 을 Parameter 에서 넘겨 받을 수 있고, Return Type 또한 Any 로 변경되어 있다. 이 이유는 Kotlin Suspend function 이 Suspend 되었을때, COROUTINE_SUSPENDED 라는 특별한 marker 를 리턴하기 때문이다.

suspend fun myFunction() { ​​​​println("Before") ​​​​delay(1000) // suspending println("After") ​​​​println("After") }

위의 함수를 Java 로 Decompile 한 코드를 보면 아래와 같다. 사실 실제로 컴파일하면 아래코드가 아닌데 조금 나만의 방식대로 수정했다.
어찌 됬든 이해하는게 더 중요하다. 이해하고 직접 코드를 찾아보고 싶다면 찾아봐도 좋다.

fun myFunction(continuation: Continuation<Unit>): Any { ​​​​val continuation = continuation ​​​​switch((continuation).label) { ​​​​​​​​case 0: ​​​​​​​​​​​​ResultKt.throwOnFailure($result); ​​​​​​​​​​​​System.out.println("Before"); ​​​​​​​​​​​​(continuation).label = 1; // 다음에 들어올때는 label 1 ​​​​​​​​​​​​if (DelayKt.delay(1000L, (Continuation)$continuation) == COROUTINE_SUSPENDED) { ​​​​​​​​​​​​​​​​return COROUTINE_SUSPENDED; ​​​​​​​​​​​​} ​​​​​​​​​​​​break; ​​​​​​​​case 1: ​​​​​​​​​​​​ResultKt.throwOnFailure($result); ​​​​​​​​​​​​break; ​​​​​​​​default: ​​​​​​​​​​​​throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); ​​​​} ​​​​System.out.println("After"); ​​​​return Unit.INSTANCE; }

Label

이 코드를 차근차근 하나씩 분석해보자. continuation.label 은 무엇일까? 우리가 PC Register 를 공부하다 보면, code 0 과 같은 부분을 마주한다.
PC Register 는 다음으로 Fetch 될 명령어의 주소를 가지고 있다. 즉, Kotlin Coroutine 에서도 복귀했다가 돌아올때, 어느지점 부터 실행해야 하는지를 알아야 하는데
이를 Label 로 관리
한다. 보면 Label 이 0 인경우 1인 경우에는 어떤 걸 실행해야해에 관한 Guide 가 Switch 문 안에 적혀있다.

COROUTINE_SUSPENDED

COROTINE_SUSPENDED 는 무엇일까? 진짜 말 그대로 Suspend Function 이 suspend 되었다는 뜻이다. NonBlocking Structure 를 공부해봤다면 왜 Return 을 하는지 알 수 있을 것이다. Thread 에게 함수가 끝난것처럼 속이기 위함이다. 여하튼 suspend 되었다면 COROTINE_SUSPENDED 를 리턴하고, 다시 Resume 될때 label 1 로 진행되게 된다. 왜냐하면 label 을 1로 바꿔줬기 때문이다. 이렇게 Suspend 에서 Return 을 해주기 때문에 Thread 를 Release 할 수 있는 이유이다. 사실 이건, 다른 언어에서도 많이 차용한다. -1 을 리턴한다거나 Exception 를 리턴한다거나 등의 방식을 많이들 이용하는 것으로 알고 있다.

Store state

그렇다면 상태를 어떻게 저장할까? 예를 들면 하나의 Function 안에서 Thread 라면 Local Stack 에 Variable 의 값을 저장하고 있을텐데, 이를 어떻게 다른 Thread 에게 알려줄 수 있을까? 이것도한 똑같다. Continuation 에 저장한다. 아래 코드를 한번 보자.

suspend fun myFunction() { ​​​​println("Before") ​​​​var counter = 0 ​​​​delay(1000) // suspending ​​​​counter++ ​​​​println("Counter: $counter") ​​​​println("After") }
fun myFunction(continuation: Continuation<Unit>): Any { ​​​​val continuation = continuation ​​​​switch((continuation).label) { ​​​​​​​​case 0: ​​​​​​​​​ResultKt.throwOnFailure($result); ​​​​​​​​​System.out.println("Before"); ​​​​​​​​​counter = 0; ​​​​​​​​​(continuation).counter = counter; ​​​​​​​​​(continuation).label = 1; ​​​​​​​​​if (DelayKt.delay(1000L, (Continuation)$continuation) == var5) { ​​​​​​​​​​​​return var5; ​​​​​​​​​} ​​​​​​​​​break; ​​​​​​case 1: ​​​​​​​​​counter = (continuation).counter; ​​​​​​​​​ResultKt.throwOnFailure($result); ​​​​​​​​​break; ​​​​​​default: ​​​​​​​​​throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); ​​​​} ​​​​​​++counter; ​​​​​​String var2 = "Counter: " + counter; ​​​​​​System.out.println(var2); ​​​​​​System.out.println("After"); ​​​​​​return Unit.INSTANCE; }

Decompile 된 코드를 보면 continuation 에 counter 값을 저장하고 있음을 알 수 있다.

Call Stack Problem

위의 코드를 보면서 한가지 이상한점을 느낄 수 있다. 이렇게 단순하게 Return 해서 Thread 를 Release 해버린다면 Suspend 된 상태에서 Main Thread 가 끝나서 종료되버리는 것 아니야? 만약 a 에서 b 함수를 호출하고 b 의 결과를 이용해야 한다면, a 함수는 b 함수가 끝나고 나서 실행되야 한다. 보통 언어에서 이를 Call Stack 을 이용해서 처리한다. 하지만 Coroutine 에서는 이를 Continuation 을 통하여 처리한다.

suspend fun a() { ​​​​val user = readUser() // suspend ​​​​b() // suspend ​​​​b() // suspend ​​​​b() // suspend ​​​​println(user) } suspend fun b() { ​​​​for (i in 1..10) { ​​​​​​​​c(i) ​​​​} } suspend fun c(i: Int) { ​​​​delay(i * 100L) ​​​​println("Tick") } suspend fun readUser(): String { ​​​​return "user" }

이 코드를 보면 a() 에는 readUser() 를 포함해서 호출되는 suspend() 함수가 많다. 이 모든 부분들이 실행되야 하므로 Decompile 해보면 아래와 같은 코드가 나온다.

fun a(continuation: Continuation<Unit>): Any { ​​​​Object $continuation; ​​​​String user; ​​​​label40: { ​​​​​​​​Object COROUTINE_SUSPENDED; ​​​​​​​​label39: { ​​​​​​​​​​​​label38: { ​​​​​​​​​​​​​​​​Object $result = (continuation).result; ​​​​​​​​​​​​​​​​Object readUserResult; ​​​​​​​​​​​​​​​​switch((continuation).label) { ​​​​​​​​​​​​​​​​case 0: // 첫번째 실행 ​​​​​​​​​​​​​​​​​​​​ResultKt.throwOnFailure($result); ​​​​​​​​​​​​​​​​​​​​(continuation).label = 1; ​​​​​​​​​​​​​​​​​​​​readUserResult = readUser((Continuation)$continuation); ​​​​​​​​​​​​​​​​​​​​if (readUserResult == COROUTINE_SUSPENDED) { ​​​​​​​​​​​​​​​​​​​​​​​​return COROUTINE_SUSPENDED; ​​​​​​​​​​​​​​​​​​​​} ​​​​​​​​​​​​​​​​​​​​break; ​​​​​​​​​​​​​​​​case 1: // 두번째 실행 ​​​​​​​​​​​​​​​​​​​​ResultKt.throwOnFailure($result); ​​​​​​​​​​​​​​​​​​​​readUserResult = $result; ​​​​​​​​​​​​​​​​​​​​break; ​​​​​​​​​​​​​​​​case 2: // 세번째 실행 ​​​​​​​​​​​​​​​​​​​​user = (String)(continuation).user; ​​​​​​​​​​​​​​​​​​​​ResultKt.throwOnFailure($result); ​​​​​​​​​​​​​​​​​​​​break label38; ​​​​​​​​​​​​​​​​case 3: // 4번째 실행 ​​​​​​​​​​​​​​​​​​​​user = (String)(continuation).user; ​​​​​​​​​​​​​​​​​​​​ResultKt.throwOnFailure($result); ​​​​​​​​​​​​​​​​​​​​break label39; ​​​​​​​​​​​​​​​​case 4: // 5번째 실행 ​​​​​​​​​​​​​​​​​​​​user = (String)(continuation).user; ​​​​​​​​​​​​​​​​​​​​ResultKt.throwOnFailure($result); ​​​​​​​​​​​​​​​​​​​​break label40; ​​​​​​​​​​​​​​​​default: ​​​​​​​​​​​​​​​​​​​​throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); ​​​​​​​​​​​​​​​​} ​​​​​​​​​​​​​​​​user = (String)readUserResult; ​​​​​​​​​​​​​​​​(continuation).user = user; ​​​​​​​​​​​​​​​​(continuation).label = 2; ​​​​​​​​​​​​​​​​if (b((Continuation)$continuation) == COROUTINE_SUSPENDED) { // b 실행 ​​​​​​​​​​​​​​​​​​​​return COROUTINE_SUSPENDED; ​​​​​​​​​​​​​​​​} ​​​​​​​​​​​​} ​​​​​​​​​​​​(continuation).user = user; ​​​​​​​​​​​​(continuation).label = 3; ​​​​​​​​​​​​if (b((Continuation)$continuation) == COROUTINE_SUSPENDED) { // b 실행 ​​​​​​​​​​​​​​​​return COROUTINE_SUSPENDED; ​​​​​​​​​​​​} ​​​​​​​​} ​​​​​​​​(continuation).user = user; ​​​​​​​​(continuation).label = 4; ​​​​​​​​if (b((Continuation)$continuation) == COROUTINE_SUSPENDED) { // b 실행 ​​​​​​​​​​​​return COROUTINE_SUSPENDED; ​​​​​​​​} ​​​​} ​​​​System.out.println(user); ​​​​return Unit.INSTANCE; }

위의 코드를 보면 기존 CallStack 처럼 a -> b -> c 순으로 실행함을 확인할 수 있다. 다만 반대로 Suspending 됬다가 올라올때는 c -> b -> a 순으로 resume 된다.

공부해보면서 느낀 점.

Kotlin Coroutine 에 대한 어느정도 청사진이 잡힌것 같다. 하지만 이 책을 읽으면서 느낀건, 번역하기 꽤나 어려워서 이게 한국시장에 늦게 들어올것 같다는 생각도 많이 들었다. 그럼에도 불구하고 읽어보면 좋은 책일 것 같다. 이책을 빨리 다 읽고, 코틀린 동시성 프로그래밍도 한번 읽어봐야겠다.


잡답

NodeJS 쪽에도 CLS 라는 Library 가 있는데, 이건 Suspend 되는 건 아니지만 Continuos 한 Object 를 계속해서 넘겨 받으면서
하나의 Function 에서 시작된 Callback Pattern 안에서 주고 받으면서 사용이 가능하다. Kotlin Coroutines 의 Function 에서 Continuation 을 주고 받는걸 보면서 약간 비슷하네 싶긴했다.

참고문헌

https://kt.academy/book/coroutines

728x90

'Kotlin' 카테고리의 다른 글

Kotlin) 변성  (0) 2022.06.30
Coroutine Builder  (0) 2022.06.27
코드스피츠 코틀린 3강 정리  (0) 2022.06.18
코드스피츠 코틀린 2강 정리  (0) 2022.06.14
코드스피츠 1강 정리  (0) 2022.06.12