Coroutine
Coroutine은 "Co" + "Routine"으로 해석할 수 있습니다.
"Co"는 "Cooperative"에서 왔고 Cooperative은 협력이라는 뜻을 가지고 있습니다.
그렇다면 "Routine"은 무엇일까요?
중괄호 상의 코드 뭉치를 의미하는데, 이 코드 뭉치를 함수나 메서드들로도 나타낼 수 있습니다.
쉽게 이해를 하기 위해서 루틴은 함수와 비슷하다라고 생각하겠습니다.
두개의 단어를 이어보자면, 협력적인 함수이라고 할 수 있겠네요?
"협력적이다"라는 것이 바로는 이해가 되지 않습니다. 협력적이라는 것이 무엇을 뜻하는 걸까요?
위키에서 따온 코루틴의 정의를 보면 다음과 같습니다.
코루틴은 실행을 일시 중단하고 재개할 수 있도록 하여 비선점형 멀티태스킹을 위한 서브루틴을 일반화하는 컴퓨터 프로그램 구성 요소입니다.
서브루틴은 위에서 설명한 루틴을 세부적으로 나눈 루틴이라고 할 수 있겠습니다. 비선점형 멀티태스킹은 협력형 멀티태스킹이라고도 불린다고 합니다. 따라서 Coroutine의 "Co"는 "협력형"에서 왔고, 비선점형 멀티태스킹을 의미한다고 할 수 있습니다. 그렇다면 비선점형 멀티태스킹이 무엇일까요?
비선점형 멀티태스킹
이를 알기 전에 잠깐 Task에 대해 알 필요가 잇습니다.
Task는 프로그래밍 세계에서 특정 할일을 나타냅니다.
Task는 프로세스와 쓰레드로 이루어집니다. OS는 이 테스크들을 어떻게 동작시킬지 결정하게 됩니다.
동작을 결정하는 과정에 스케줄러가 사용되게 됩니다. 스케줄러는 Cpu의 공간을 어떻게 잘 활용 할 수 있을 지 결정합니다.
이 결정 과정에서 다양한 스케쥴링 기법이 들어가는데 크게 선점형과 비선점형으로 나뉘게 됩니다.
선점형이란 태스크가 CPU를 할당받아 실행 중에 있어도 중지하고 할당받았던 CPU자원을 강제로 뺏어 다른 태스크에 할당할 수 있다는 것을 의미합니다. 대부분의 운영체제는 선점형 스케쥴링 방식을 사용하고 있습니다.
반면에 비선점형이란 어떤 태스크가 CPU를 할당받아 실행 중이라면, 이 태스크가 스스로 종료되거나 반납할때까지 계속 사용하는 것을 보장하는 것을 말합니다.
반납이라는 것은 태스크들이 자발적으로 양보를 하는 것을 말하는데 이 부분에서 협력형이다라고 할 수 있을 것 같습니다.
Coroutine은 비선점형 스케쥴링 기법을 이용하여, 여러개의 태스크들이 실행될 수 있도록 합니다.
Coroutine의 특징
코루틴은 다음과 같은 특징을 가진다고 합니다.
- 가볍다(경량)
- 동시성 프로그래밍
- 구조적 동시성
경량 스레드
코루틴은 경량 스레드라고도 불리는데 왜 가볍다고 할까요?
이해를 하기 위해서는 스레드와 코루틴의 차이에 대해 알 필요가 있습니다.
스레드
스레드는 OS 스케줄러에 의해 관리 됩니다. 안드로이드는 리눅스 OS로 구현이 되고, 리눅스는 기본적으로 CFS(Completely Fair Scehduler) 스케줄링 방식을 사용합니다.
CFS는 Task에 공정한 CPU 시간을 할당하는 선점형 방식를 사용합니다.
스레드 A에서 실행되던 태스크 1이 태스크2의 값이 필요하다면, 스레드 B로 전환이되고,
이전의 스레드 A는 Blocking되어 스레드 B의 태스크2의 값이 올때동안 이용하지 못합니다.
싱글코어라면 동시에 실행될 수 있는 태스크는 1개 뿐임으로, 선점형 스케줄링에 의해서 각각의 작업의 실행을 분배합니다.
위의 선점형 방식 때문에 프로그래밍할 때 순차적이지 않아, 공유 공간의 데이터의 원자성을 보장할 수 없습니다.
var count = 0
fun main() {
val thread1 = Thread(LogThread())
val thread2 = Thread(LogThread())
val thread3 = Thread(LogThread())
val thread4 = Thread(LogThread())
val thread5 = Thread(LogThread())
thread1.start()
thread2.start()
thread3.start()
thread4.start()
thread5.start()
}
class LogThread() : Runnable {
override fun run() {
count++
println(Thread.currentThread().name + " $count")
}
}
/*
Thread-0 2
Thread-3 5
Thread-4 4
Thread-1 3
Thread-2 3
*/
또한 스레드는 stack을 제외한 영역을 공유하는 특징을 가지고 있습니다.
따라서 스레드간의 전환이 필요할 때, stack영역만 교체를 하면 되므로, context switching에 대한 비용이 process context switching보다 저렴합니다.
( process context switching은 code ,data , heap ,stack 을 전부 바꿔야함).
stack에는함수가 호출되면 함수의 매개변수, 호출이 끝난 뒤 돌아갈 반환 주소값, 함수에서 선언된 지역 변수등이 저장됩니다.
thread 간 context swithcing에서 상태를 TCB( thread control block) 에 저장되고 그 안에는 스택 포인터, PC(Program counter = 현재 스레드의 명령어 ),PCB( process control block)에 대한 포인터 등이 존재합니다.
TCB 안에 저장된 값들 때문에, context switching 이후 돌아와도 TCB에 저장된 스택 포인터와 PC를 통해 이전에 진행했던 상태를 복원하고 이어서 실행할 수 있습니다.
Coroutine
코루틴은 기본적으로 하나의 스레드 내부에서 동작합니다.
다음의 이미지를 보면 조금 더 이해가 쉽습니다.
코루틴의 장점은 Task를 Object로 관리한다는 점입니다. 이러한 특징으로 Coroutine은 stack less라고 설명됩니다.
동일한 스레드에서 작업간 전환이 일어날때 Coroutine Object만 바꿔주면 되기 때문에 위에서 쓰레드에서 일어나는 Context Swithching은 일어나지 않습니다.
그렇다면 동시성 프로그래밍은 어떤 것을 뜻할까요?
동시성 프로그래밍
coroutine의 suspend keyword는 잠시 중단하여 언젠가 다시 재개(resume)될 수 있다는 것을 의미합니다. suspend를 만나게 된다면 함수를 잠시 탈출하고 이 함수가 실행되는 스레드를 차단하지 않습니다.
suspend fun fetchUser(id: String) {
val token = auth() // suspend function
val user = getUser(token.id) // suspend function
updateUserData(user) // suspend function
}
fun main() {
launch {
fetchUser(id)
}
println("hi")
}
auth()는 suspend function이므로 fetchUser()를 탈출하게 됩니다. 스레드를 차단하지 않으므로, hi가 출력이 먼저 됩니다.
하지만 auth()는 어디선가 계속 실행되고 있습니다.
다른 코드들이 실행이 되더라도, auth()가 끝나면 다시 fetchUser()로 진입해서 auth()밑의 부분부터 실행을 하게 됩니다.
이것이 코루틴이 비선점형 멀티태스킹이 가능한 이유입니다. suspend를 만남으로써 cpu의 점유를 스스로 반환을 하는 것입니다.
이러한 특성은 동시성 프로그래밍이 가능하게 해줍니다.
suspend를 만나면 탈출을 하는 것을 알겠고, 어떻게 되돌아올 수 있는 거지?라고 의문이 듭니다.
suspend 키워드가 설정된 함수를 컴파일해보면 함수의 매개변수에 continuation이 들어가 있는 것을 볼 수 있습니다.
CPS(Continuation Passing Style)
suspend fun B(){}
@Nullable
public final Object B(@NotNull Continuation var1) {...}
public interface Continuation<in T> { // 함수가 리턴하고하는 값
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
이 Continuation은 인터페이스로 코루틴을 재개하기 위한 함수가 포함되어 있습니다.
suspend fun fetchUser(id: String) {
val token = auth() // suspend function
val user = getUser(token.id) // suspend function
updateUserData(user) // suspend function
}
labeling in suspend
fun fetchUser(id: String, cont : Continuation){
...
val continuation = object: ContinuationImpl(cont){...} // state machine
switch(continuation.label){
case 0:
continuation.id = id
continuation.label = 1
auth(continuation) // continuation.result에 값저장
case 1:
val id = contination.id
val token = contination.result as String
continutaion.label =2
val user = getUser(token.id, continuation)
case 2:
updateUserData(user)
}
}
다시 상기하자면 coroutine은 stackless입니다. 그렇다면 stack을 참조해서 기존의 상태를 복원했는데, 이를 어떻게 복원하는지 할까요?
fun fetchUser()를 실행한 모습입니다.
suspend fucntion들은 각각의 case로 변환되게 됩니다.
그리고 label에 따라서 실행되는 함수들이 나뉘어 지게 됩니다.
그리고 상태를 저장할 continuation이라는 객체를 만듭니다.
처음 함수가 시작될 때는 label이 0입니다.
label이 0일때 함수의 매개변수와 label의 값을 올려주고, auth를 실행하고 함수를 나갑니다.
auth에 전달된 continuation에는 auth의 결과값이 저장됩니다.
fetchUser가 다시 실행이 되면, label이 1로 case 1 이 실행됩니다. 함수를 나갔다가 돌아왔기 때문에 continuation에 저장된 값들을 복원합니다.
따라서 coroutine은 CPS를 통해서 다음을 한다고 할 수 있겠습니다.
- labeling을 통해 코드의 실행 위치를 처리.
- 함수에 대한 진입/진출 시 상태를 저장 복원하기 위한 continuation.
구조적 동시성?
구조적 동시성에서 말하는 동시성은 위에서 코루틴을 이동하면서 동시에 실행되는 것처럼 구현한다의 동시성이 아닙니다.
구조적이라는 단어에 집중해야합니다.
코루틴의 구조적 동시성이라는 것은 CoroutineScope내부에서만 코루틴을 실행할 수 있음을 이야기합니다
예를 들어 수없이 많은 코루틴을 작성했다고 했을때, 수많은 코루틴의 생명주기를 추적한다면 매우 힘든 일 것입니다.
코루틴이 취소되어야하는데, 계속 실행중이라면 이는 메모리 누수로도 이어질 것입니다.
코루틴 스코프를 사용한다면, 상위 스코프를 닫는 행위로 상위 스코프 내부에 작성된 모든 하위 코루틴들을 종료시킬 수 있습니다.
CoroutineScope는 coroutine Context로 구성됩니다. coroutineContext는 Dispatcher, name, exceptionHandler, job이 존재합니다.
코루틴 스코프 내부에서 다른 코루틴이 시작이 된다면, 새로운 코루틴은 상위 코루틴의 자식이 됩니다.
coroutine builder(launch ,async , runblocking)은 job을 반환하는데, 이 job들은 CoroutineScope에 종속적이게 됩니다. job은 내부에
public val children: Sequence<Job>
자식들을 가집니다. 따라서 부모 - 자식의 관계를 가지고 부모 스코프를 닫는 것으로 자식 코루틴을 모두 종료할 수 있는 기능을 가집니다.
마무리
자주 쓰던 코루틴에 대해서 글을 작성해보았는데요. 왜 사용하는지에 대해서 알아볼 수 있는 좋은 시간이였다고 생각합니다. 코루틴에 대해 알아보면서 CS까지
공부해볼 수 있었던 좋은 기회였던것 같습니다.
출처
코루틴 공식 가이드 자세히 읽기 — Part 1 — Dive 3
DroidKnights 2018 도창욱 Kotlin 코루틴은 어떻게 동작하는가?
'Android' 카테고리의 다른 글
Activity (0) | 2023.02.22 |
---|---|
Android에서 잠금화면 만들어보자 (0) | 2023.01.19 |
Notification을 수신해보자! (0) | 2022.12.08 |
LiveData를 뜯어보자 (0) | 2022.11.23 |
Adapter Memory Leak (0) | 2022.05.31 |