본문 바로가기
Android

[Android/Kotlin] Coroutine은 왜 Light Thread이고 어떤 식으로 동작할까?

by 옹구스투스 2022. 9. 19.
반응형


제가 가진 의문과 생각을 정리한 글입니다~!

틀린 내용이 있다면 알려주세요 :)


Coroutine이란?

Coroutine은 협력형 멀티 태스킹과 루틴을 통해서 구조화된 동시성을 구현하는 것이다.

협력형 멀티태스킹?

우선 Coroutine은 비선점형 스케쥴링이다.

비선점형이란 멀티태스킹의 각 작업을 운영체제가 강제로 중단시키고 다른 작업을 실행하게 할 수 없는 것을 말하는데,

그럼에도 불구하고 Coroutine이 여러 작업을 교체하며 수행할 수 있는 이유는 각 Coroutine들이 자발적으로 CPU 자원을 포기하며(yield) 여러 Coroutine이 협력적으로 실행을 주고받으면서 작동하기 때문이다.

 

구조화된 동시성?

Coroutine은 CoroutineScope을 제공해주는데, 이 스코프 내에 Coroutine들이 구조적으로 Parent Child 관계를 맺으며 작업의 취소, 예외 등을 전파하며 구조적으로 관리할 수 있다.

 

Coroutine이 어떤 개념인진 대충 알겠다.

그럼 좁은 의미로 Coroutine이란 무엇일까?

일시 중단 가능한 작업의 단위로 Heap 영역에 할당되며 Light Thread라고 불린다.

Coroutine의 핵심 개념으로 일시 중단 (suspend), 재개(resume)가 있다.

Coroutine에서 작업1을 실행하다가 일시 중단하고, 다른 작업2를 수행, 다시 작업1을 재개하는 것을 생각하면 된다.

오~ Thread처럼 동작한다. Thread와 Coroutine 모두 동시성을 보장하기 위한 기술이니 당연하다.

그럼 무엇이 다를까?

위에서 말한 것처럼 Coroutine은 Heap 영역에 할당되어 Switching 단위가 Object 단위인 것이다.

OS 레벨의 Context-Switching은 Thread 단위이고 Thread 위에서 동작하는 Coroutine은 Object 단위이기 때문에 Thread에 비해 Light하다!

추가로 단순 성능만 따지면 Procces, Thread, Coroutine 단위의 Context Switching을 줄 세울 수 있을 것 같다.

Process -> Thread
(Process는 각각 독립된 영역을 갖는 반면 Thread는 일부 영역을 공유한다. ex: Heap)
Thread -> Coroutine
(Coroutine에 비해 Thread는 사용하는 메모리 자체가 많다. ex: Stack)

하지만 이것이 Coroutine이 Thread에 비해 항상 성능이 우월함을 의미하진 않는다.

메모리를 덜 사용하게끔 설계됐다는 것이 좀 더 맞는 말이다.

스택오버플로우 내용에 따르면 시나리오에 따라 다른 것 같다.

 

프로젝트에 Coroutine을 사용한다고 해서 Thread Context Switching이 없다고 할 수 있을까?

우리는 Coroutine을 사용할 때 Dispatcher를 이용해 CPU Bound, IO Bound 등 적절한 Thread로 작업을 Dispatch한다.

따라서 Coroutine 자체의 중단/재개는 Programmer 레벨의 Context Switching이지만 우리는 Thread도 같이 사용하고 있기 때문에 Coroutine을 사용하면 Context Switching (Thread)가 없다? 는 아니라고 본다.

 

그럼 위의 성능 관련 시나리오를 생각해 보면, 각각의 Coroutine마다 새로운 Thread에서 실행되게끔 한다면?

이런 경우가 Coroutine을 효율적으로 사용하지 못한 케이스라고 생각한다.

 

Corutine 동작 방식

그럼 이제 궁금한 것은 Coroutine은 스레드처럼 동시성을 보장하기 위해 중단/재개할 수 있고 이는 Programmer 레벨의 Context Switcing이라고 했는데, 이 Programmer 레벨의 Context Switching은 어떻게 이루어지는 것인가??

결론부터 말하면 Coroutine은 CPS(Continuation Passing Style) 방식으로 작업을 중단 및 재개한다.

말 그대로 Continuation을 pass~하는 Style인 거다.

Continuation엔 뭐가 있는데??

 

Thread Context Switching이 일어날 땐 스택의 참조 값을 바꾸고 PC와 레지스터에 관한 값을 가진 TCB(Thread Control Block)를 저장 및 참조를 변경한다.

 

Continuation도 이러한 작업 정보가 들어있다.

프로세스는 PCB

스레드는 TCB

코루틴은 CCB 라고 부르고 싶어져 버려..

/**
 * Interface representing a continuation after a suspension point that returns a value of type `T`.
 */
@SinceKotlin("1.3")
public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */
    public fun resumeWith(result: Result<T>)
}

실제 suspend 함수를 디컴파일해보면, suspend 키워드는 사라지고 매개 변수로 Continuation 객체를 넘겨받는다.

Continuation 객체에는 이전까지 실행했던 작업 상태, 다음 수행할 작업 등이 있다.

함수 블럭에선 switch-case를 이용한 Labeling을 통해 case0을 실행하고 중단, 다시 resume되면 case1을 실행하고 중단하는 방식으로 Context Switching이 일어난다~!

자세한 동작 방식은 이전 드로이드 나이츠에서 도창욱님께서 잘 설명해주고(하단 링크), 최근에 코틀린 나이츠에서도 이에 관한 발표가 있었는데 이건 다시 보기가 없는 것 같다. 추가적으로 CPS 및 State Machine에 관한 링크도 첨부한다. 참고로 Kotlin In Action에 깊이 있는 내용이 있진 않다. Coroutine이 코틀린에 한정적인 기술은 아니기 때문일까~?

 

Corutine이 왜 Light Thread라 불리고 어떤 식으로 동작하는지 간략하게 정리해보았다.

아무쪼록 Coroutine은 Thread 위에서 동작하며 작업의 단위가 Thread가 아닌 Coroutine이라는 단위이니 Coroutine이 일시 중단이나 delay한다고 해서 Thread가 sleep()처럼 블락되지 않는다~

 

이제 스레드를 블락하지 않고 Light한 코루틴으로 적절한 Dispatcher를 사용하며 재밌게 비동기 프로그래밍을 해보자.

launch, async-await, withContext 등 코루틴을 효율적으로 사용하기 위한 재밌는 내용이 많다~!~!

 

References

- Kotlin In Action

- https://www.youtube.com/watch?v=usaD7HyN598&t=1941s

- https://medium.com/google-developer-experts/coroutines-suspending-state-machines-36b189f8aa60

- https://aaronryu.github.io/2019/05/27/coroutine-and-thread/

반응형

댓글