Coroutine 스터디를 진행하며 공부한 내용들에 대해 정리해보려고 합니다.
CPS (Continuation Passing Style)
함수형 프로그래밍에서 Continuation의 전달로 프로그램 제어가 흐르게하는 프로그래밍 스타일
말 그대로 Continuation을 전달하는 스타일 입니다.
CPS 사용 이유
- 비동기 작업을 더 직관적이고 쉽게 관리
- 복잡한 콜백을 switch문으로 단순하게 표현함
- 함수형 프로그래밍
- 함수적 스타일로 상태와 흐름을 효과적으로 관리
- 스택 스페이스 절약
- 꼬리 호출을 사용하기 때문에, 서로 중첩된 함수를 스택 프레임에 보관하지 않고도 긴 재귀를 표현
- 이를 통해 스택 오버플로우 방지
여기서 꼬리 호출이란?
함수 내에서 마지막으로 수행되는 작업이 또 다른 함수 호출일 때, 이를 꼬리 호출 이라고 합니다.
이런 경우 컴파일러에서 꼬리 호출이 있음을 인식하면, 새로운 스택 프레임을 쌓는 대신 현재 스택 프레임을 재사용할 수 있습니다.
이를 통해 스택 깊이가 고정되며, 스택 오버플로우가 발생하지 않습니다.
Continuation
프로그램 상태의 추상적 표현
Continuation 은 프로그램 제어 상태를 실체화(reify)
Continuation의 역할
- 상태 저장: Continuation은 suspend 함수가 중단된 지점에서의 상태를 저장하고 이후에 해당 코루틴이 재개될 때 사용
- 결과 전달: Continuation을 사용하여 suspend 함수가 완료된 후 그 결과를 호출자에게 전달
- 코루틴 재개: Continuation의 resume 메서드를 호출하면, suspend 함수는 중단된 지점에서 다시 시작됨
Suspend 내부 동작
그렇다면 Continuation은 어떻게 전달될까요?
함수 앞에 suspend 키워드를 붙이면 컴파일 될 때 컴파일러가 suspend 키워드를 지우고, 파라미터로 Continuation 객체를 붙이게 됩니다.
예를 들어 이런 코드가 있다고 합시다.
fun main() {
CoroutineScope(Dispatchers.Default).launch {
postItem(Item("1", 100))
}
}
suspend fun postItem(item: Item) {
val token = getToken()
val post = createPost(token, item)
}
여기의 postItem 메서드를 Decompile 해보면 아래와 같이 파라미터에 Continuation이 추가된 것이 보이실 겁니다.
CPS에서 말하는 Continuation의 전달이 되는 것입니다.
알아보기 힘든 변수 이름을 변경하고, 주석으로 설명을 달아두었으니 천천히 따라가 보시면 됩니다.
Decompile
// 파라미터에 Continuation이 추가됨
public final Object postItem(@NotNull Item item, @NotNull Continuation var2) {
Object $continuation;
label27: {
// var2가 postItem에서 생성된 Continuation이 맞는지 확인
if (var2 instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)var2;
/*
최상위 비트가 1인지 확인 (1이면 resume된 상태,
0이면 suspend 함수가 처음 실행된 상태)
*/
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
// 최상위 비트가 1이면, 다시 0으로 초기화하고 label27 블록 나감
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label27;
}
}
// continuation 구현부, continuation 객체 생성
$continuation = new ContinuationImpl(var2) {
Object result;
int label;
Object L$0; // 현재 객체(클래스의 인스턴스) 참조
Object L$1; // item 값
// resume 될때 invokeSuspend 호출됨
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
// 최상위 비트 1로 설정
this.label |= Integer.MIN_VALUE;
return Playground.this.postItem((Item)null, this);
}
};
}
label22: {
Object $result = ((<undefinedtype>)$continuation).result;
// coroutine 상태
Object isSuspended = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Object tempResult;
switch (((<undefinedtype>)$continuation).label) {
case 0:
// 처음으로 postItem이 호출될 때 실행됨
ResultKt.throwOnFailure($result);
((<undefinedtype>)$continuation).L$0 = this;
((<undefinedtype>)$continuation).L$1 = item;
((<undefinedtype>)$continuation).label = 1;
tempResult = this.getToken((Continuation)$continuation);
/*
COROUTINE_SUSPENDED 값이 리턴되면, postItem은 이 지점에서 멈춤
getToken 함수가 비동기 작업을 완료하고 결과를 제공하면,
코루틴은 invokeSuspend 메서드를 통해 재개됨
*/
if (tempResult == isSuspended) {
return isSuspended;
}
break;
case 1:
//getToken의 비동기 작업이 완료된 후 코루틴이 재개될 때 호출
item = (Item)((<undefinedtype>)$continuation).L$1;
this = (Playground)((<undefinedtype>)$continuation).L$0;
ResultKt.throwOnFailure($result);
tempResult = $result;
break;
case 2:
ResultKt.throwOnFailure($result);
break label22;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
// case 2 일 때 실행됨
String token = (String)tempResult;
((<undefinedtype>)$continuation).L$0 = null;
((<undefinedtype>)$continuation).L$1 = null;
((<undefinedtype>)$continuation).label = 2;
if (this.createPost(token, item, (Continuation)$continuation) == isSuspended) {
return isSuspended;
}
}
Unit var4 = Unit.INSTANCE;
return Unit.INSTANCE;
}
위의 최상위 비트 확인 코드 설명입니다.
자바에서 int는 32비트 정수이므로, 최상위 비트는 31번째 비트입니다.
그러므로 Integer.MIN_VALUE는 아래와 같습니다.
최상위 비트가 1이면 음수, 0이면 양수 입니다.
Integer.MIN_VALUE == 0b10000000_00000000_00000000_00000000 // 16진수로는 0x80000000
- label & Integer.MIN_VALUE → 최상위 비트가 1인지 검사 (이미 재개된 상태인지 확인)
- label -= Integer.MIN_VALUE → 최상위 비트를 0으로 초기화 (초기 실행 상태로 설정)
- label |= Integer.MIN_VALUE → 최상위 비트를 1로 설정 (코루틴이 재개되었음을 표시)
IntrinsicsKt.getCOROUTINE_SUSPENDED() 이 코드를 실행하게 되면 아래의 Enum 클래스에서 값이 리턴되며, 코루틴 상태에 따라 대기(COROUTINE_SUSPENDED)할지, 다음으로 진행할 지(RESUME)를 판단합니다.
@kotlin.SinceKotlin @kotlin.PublishedApi
internal final enum class CoroutineSingletons private constructor() : kotlin.Enum<kotlin.coroutines.intrinsics.CoroutineSingletons> {
COROUTINE_SUSPENDED,
UNDECIDED,
RESUMED;
}
위의 내용을 JetBrain 개발자가 설명해주는 유튜브 영상 입니다.
'Android' 카테고리의 다른 글
[Android] Bitmap 이미지 블러 처리하기 (RenderNode) (0) | 2025.01.17 |
---|---|
[Android] Compose의 remember 그리고 MutableState (TextField 값 바꾸기) (2) | 2024.12.20 |
[Android] Compose 사용 이유, 맛보기 (0) | 2022.07.08 |
[Android] Context란? (0) | 2021.10.22 |
[Android] 레이아웃 Background 둥글게 만들기 (shape, radius, border stroke) - XML (1) | 2021.03.30 |