일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Entity
- mysql
- mirror maker2
- K8s
- Kubernetes
- producer
- bean
- transactionaleventlistener
- Streams
- JPA
- Spring JPA
- topic생성
- CodePipeline
- centos7
- kafka
- spring kafka
- consumer
- ECS
- API
- Spring Data JPA
- offsetdatetime
- entity graph
- cd
- spring
- PAGING
- CI
- AWS
- QueryDSL
- git
- Kotlin
- Today
- Total
Yebali
Coroutine 톺아보기 본문
Coroutine 이란?
위키의 표현을 빌리자면 coroutine은 실행을 일시중단(suspend)하고 재개(resume)할 수 있도록 하여
비선점형 멀티태스킹을 위한 서브루틴(subroutine)을 일반화하는 컴퓨터 프로그램 구성요소이다.
서브루틴(Subroutine)
서브루틴(subsroutine)이란 routine 내에서 실행되는 또 다른 routine으로 함수, 메서드 등을 의미하는 포괄적인 용어이다.
비선점형 멀티태스킹(Non-preemptive multi tasking)
컴퓨터 내부에서 실행되는 프로세스들은 한정된 CPU, 메모리 등의 자원을 사용하기 위해 서로 경쟁한다.
이때 OS은 하나의 프로세스가 자원을 무한정으로 점유하는 것을 막고 자원을 효율적으로 사용하기 위해
스케쥴링을 하는데, 스케쥴링 방식에는 크게 선점형(Preemptive)과 비선점형(Non-Preemptive) 방식이 있다.
선점형(Preemptive) 방식은 다른 프로세스가 자원을 사용하고 있을 때 강제로 빼앗아 사용할 수 있는 방식이며
대표적인 스케쥴링 방법은 Round Robin, Shortest Remaining Time 등이 있다
비선점형(Non-Preemptive) 방식은 특정 프로세스가 작업이 끝나거나 실행권한을 내려놓을 때까지 자원을 독점하는 방식이다.
이때 다른 프로세스는 자원을 빼앗을 수 없다.
대표적인 스케쥴링 방법은 First Come First Out, 우선순위(Priority), Shortest Job First 등이 있다.
즉 비선점형 멀티태스킹은 다른 프로세스에게 자원을 빼앗기지 않으면서
여러 작업을 동시에 하거나 동시에 하는 것처럼 보이게 하는 방식이다.
Coroutine 동작 방식
coroutine에서 가장 중요한 것은 routine을 실행하는 도중 중단(suspend)하고 재개(resume)하는 것이다.
중단(suspend)과 재개(resume)가 가능하기 때문에 coroutine은 다른 coroutine이 실행될 수 있도록
코드 실행을 중단하고 자신이 실행되고 있는 thread을 양보할 수 있다. 또한, 중단된 coroutine은 다른 thread에서 재개될 수 있다.
그렇다면 coroutine은 어떻게 코드의 실행을 중단(suspend)과 재개(resume)할 수 있을까?
State Machine
coroutine은 state machine 방법론을 채택하여 구현되었다.
state machine 방법론이란 상태에 따라서 명시적으로 실행 흐름을 제어하는 방법론을 말한다.
아래 예시는 state machine 방법론을 이용한 중단 가능한 함수이다.
함수를 중단 가능하게 하기 위해서는 실행과 관련된 변수 별도로 관리해야 하는데
MyCookingStateMachine은 label이라는 변수를 사용하여 중단에 따른 실행 흐름을 제어하고 있다.
class MyCookingStateMachine {
private var label = 0
private var result: Result<String> = Result.success("")
operator fun invoke(): Result<String> {
this.result = when (label) {
0 -> {
label = 1
Result.success("물을 끓인다.")
}
1 -> {
label = 2
Result.success("면과 스프를 넣는다.")
}
2 -> {
label = 3
Result.success("그릇에 담는다.")
}
3 -> {
label = 0
Result.success("설거지를 한다.")
}
else -> {
label = 0 // init state
Result.failure(Exception("Invalid state"))
}
}
return result
}
}
fun main() {
val cookingStateMachine = MyCookingStateMachine()
println(cookingStateMachine().getOrNull()) // 물을 끓인다.
println(cookingStateMachine().getOrNull()) // 면과 스프를 넣는다.
println(cookingStateMachine().getOrNull()) // 그릇에 담는다.
println(cookingStateMachine().getOrNull()) // 설거지를 한다.
}
Suspension Point
위 예시에서 코드 실행 중 네 차례이 중단이 발생한다.
coroutine에서 코드의 실행 흐름이 중단될 수 있는 지점을 suspension point이라고 한다.
suspension point은 couroutine의 핵심 목표인 동시성 확보를 위해 매우 중요하다.
coroutine은 suspension point에서 실행을 중단하고 중단된 coroutine이 위치한 thread에서는 다른 coroutine을 실행시킨다.
예를 들어, I/O작업을 위해 CPU 유휴시간이 발생하는 지점을 suspension point로 설정하여 suspend가 발생한 couroutine을
대신하여 다른 coroutine을 스케쥴링하고 실행함으로써 CPU자원을 효율적으로 사용할 수 있다.
Kotlin에서 Kotlin 컴파일러가 suspend function을 state machine 형태로 byte code를 생성하는데
이때 'CORUOUTINE_SUSPEND'라는 특수한 enum값을 사용하여 suspension point를 생성한다.
Suspending function
suspending function은 잠재적으로 suspension point가 될 수 있는 가능성이 있는 특수한 함수이다.
그러나 모든 suspending function이 suspension point를 포함하는 것은 아니다.
예를 들어 아래 'sayHello()' 함수는 suspension point가 되지 못한다.
반면 'sayHelloAfter()'함수는 suspension point가 된다. 엄밀하게는 'sayHelloAfter()' 함수의 'delay()'함수가
suspension point에 해당한다.
import kotlinx.coroutines.delay
suspend fun sayHello() {
println("Hello!")
}
suspend fun sayHelloAfter(ms: Long) {
delay(ms)
println("Hello")
}
Continuation
continuation은 coroutine에서 실행되는 코드 블록과 execution context를 실질적으로 소유한 객체로
coroutine의 실행 및 중단과 관련된 핵심적인 역할을 수행한다.
즉, 함수를 중단하고 재개 시키위해 필요한 것들을 담아두는 객체이다.
흔히 coroutine builder로 알고 있는 'launch'나 'async'같은 함수들도 내부적으로 Continuation을 생성한다.
Continuation은 interface로 자체의 실행 환경을 담고 있는 CouroutineContext와
코드 실행을 재개하기 위한 resumeWith() 함수를 제공한다.
/**
* 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>)
}
Coroutine에서 Context Switching이 일어나지 않는 이유
coroutine은 일종의 경량 스레드 모델로 중단과 재개를 반복하며 context switching 비용과 blocking 시간을 줄여준다.

일반적으로 thread는 process에서 실제로 작업을 실행하는 단위이며 process내 text, data, heap영역의 메모리를 공유한다.
즉, thread는 자신만의 stack을 가지고 있으며 thread context switching이 일어날 때 stack과 register에 대한
context switching이 발생한다.
하지만 coroutine은 stack영역을 사용하지 않고 heap을 사용하기 때문에 stack영역에 대한 context switching 없이
동작하기 때문에 전통적인 의미의 context switching보다 더 효율적으로 동작한다.
즉, coroutine은 CPU에서 실행되기 위한 register값 정도는 switching이 발생하겠지만
전통적인 context switching은 일어나지 않는다.
물론 OS에 의해 Coroutine이 실행되는 스레드/프로세스가 다른 CPU core로 스케쥴링되면 context switching은 발생한다.
참고
- https://velog.io/@koo8624/Kotlin-Coroutine%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC
- https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md
'Kotlin' 카테고리의 다른 글
OffsetDateTime의 시간 비교 (0) | 2023.06.04 |
---|---|
Java, Kotlin의 날짜와 시간 (0) | 2022.11.28 |
Spring JPA Entity에 Data Class를 사용해도 될까? (0) | 2021.10.31 |
Kotlin의 Collections (0) | 2021.10.31 |
Kotlin의 Data/Enum 클래스 (0) | 2021.09.22 |