0. 코루틴이란?
- 코루틴은 비동기 실행을 간단하게 하기위한 concurrency design pattern이다.
- suspendable computation 기법을 사용한다.
- 함수의 실행을 특정 지점에서 일시 중단하고 나중에 다시 실행할 수 있는 기법
- suspendable computation 기법을 사용한다.
- 코루틴(Coroutine)은 코틀린(Kotlin)에만 존재하는 것이 아니다.
- Python, Go, C#, Js 등에서도 사용하는 개념이다.
1. Main Routine, Sub Routine, Co Routine
Subroutine vs Coroutine
- 일반적으로 한 함수에서 다른 함수를 실행할 때, 호출되는 함수를 서브루틴이라고 부른다.
fun register() {
...
getDeviceToken()
...
}
fun getDeviceToken() {
...
}
getDeviceToken()
함수는register()
함수의 서브루틴이다.
- 코루틴은 호출한 함수의 안에서 수행되는 것이 아니라 호출한 함수와 함께 수행되는 루틴이다.
- coroutine1안에서 coroutine2가 실행되는 것이 아니다.
- 각 코루틴은 하나의 routine이며 쓰레드를 공유하며 함께 실행되는 것이다.
- 따라서 병렬적으로 처리하는 것이 아닌 동시적으로 처리하는 기법이다.
Concurrency vs Parallelism
- CPU의 코어는 1개의 프로세스만 실행할 수 있다.
- 싱글 코어 CPU에서는 멀티태스킹을 Concurrent하게 처리한다.
- 실제로 프로세스가 동시에 실행되지 않는다.
- 시분할을 통해 동시에 실행되는 것 처럼 동작한다.
- 멀티태스킹을 Parallel하게 하려면 코어나 CPU가 여러개 있어야한다.
- 같은 시간에 여러 프로세스가 실행된다.
2. 동작방식 - H/W
Thread
- 쓰레드는 같은 프로세스 내에서 Heap, Code, Data를 공유하며, Stack영역을 독립적으로 가진다.
- Thread단위로 작업하면 context switching 비용이 프로세스에 비해 적어진다.
Coroutine
- 코루틴의 작업 단위는 Coroutine Object로 JVM의 Heap에 적재된다.
- 한 쓰레드에서 Coroutine Object만 변경되기 때문에 context switching이 필요없다.
- 프로그래밍적으로 switching을 할 수 있다.
- Coroutine을 Lightweight Thread라고도 한다.
3. 사용
코루틴은 크게 3가지로 구성된다.
- Coroutine Scope
- Coroutine Context
- Coroutine Builder
코루틴이 어떤 범위에서 동작하는 지에 대한 Scope를 정하고, 어떤 쓰레드에서 코루틴을 실행할 지 Context를 설정하고, 코루틴 객체를 만드는 Builder로 구성된다.
Scope
- CoroutineScope
- 코루틴이 필요할 때 만들고, 필요하지 않을 때 취소할 수 있는 Scope이다.
- 안드로이드에서는 라이프사이클을 따라간다.
- GlobalScope
- Application이 종료될때 까지 유지되는 Scope이다.
- Singleton으로 만들어져 있다.
- 메모리 누수가 생기기 쉽다.
Context
Dispatchers
코루틴이 실행이되는 쓰레드를 지정
- Default
- 많은 CPU 연산이 필요한 작업을 하기위한 백그라운드 쓰레드
- IO
- File, Network, DB IO를 위한 쓰레드
- Main
- UI 쓰레드
Builder
launch
- Job 객체 반환
val job: Job = CoroutineScope(Dispatchers.IO).launch {
// 비동기함수()
}
async
- Deferred 객체 반환
- 람다 안에 마지막 줄이 반환된다.
await()
메소드가 호출되면async
가 실행되고 실행을 완료하고 결과를 반환할 때 까지 기다린다.
suspend fun doWorld() { // this: CoroutineScope
val deferredInt: Deferred<Int> = CoroutineScope(Dispatchers.IO).async {
println(1)
2
}
println(3)
val result: Int = deferredInt.await()
println("$result")
println(4)
}
// 결과
3
1
2
4
runBlocking
- 현재 쓰레드를 block하고, 코루틴 함수를 실행한다.
withContext
- Dispatcher 변환
- Main 스레드에서 작업하는 중에 DB작업 같은 IO작업이 필요할때 사용할 수 있다.
CoroutineScope(Dispatchers.Main).launch {
updateUI1()
updateUI2()
withContext(Dispatchers.IO) {
insertUserData()
}
updateUI3()
}
Job & Deferred
- 코틀린에서는 Coroutine Object를 Job이나 Deferred로 정의한다.
- Deferred는 결과값을 가지는 Job이다.
- 코루틴의 상태를 가지고 있고 흐름을 관리할 수 있다.
start()
- 중단 없이 실행됨
- 실행을 기다리지 않는다. 호출한 쓰레드가 종료되면 같이 종료
join()
- Job의 동작이 완료될 때 까지 코루틴 일시 중단
cancel()
- 코루틴을 종료하도록 유도한다. 대기하지 않는다.
cancelAndJoin()
- 코루틴 종료 신호를 보내고 종료할 때 까지 기다린다.
cancelChildren()
- 자식 코루틴 종료
- Job states cycle
wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
예외처리
- CoroutineExceptionHandler를 사용하여 예외처리를 할 수 있다.
- 코루틴 안에서 예외 발생 시 exception을 handler로 전달하여 처리하는 흐름을 만들 수 있다.
- launch vs async
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
4. 예시
- 앱에 로그인 했을 때, Sendbird User를 만들고 디바이스 토큰을 등록하는 상황
- 유저등록이 완료된 후, 디바이스 토큰을 등록해야 한다.
- callback
fun initializeSendbird() {
createUser() {
registerToken() {
doSomething()
}
}
}
- rx
fun initializeSendbird() {
Observable
.just(...)
.observeOn(MAIN_Thread)
.subscribeOn(IO_Thread)
.flatMap { () -> createUser() }
.flatMap { () -> registerToken() }
.subscribe( { () -> doSomthing() }, { fail() })
}
- coroutine
- 비동기 작업을 순차적으로 작성할 수 있다.
initializeSendbird()
함수가 코루틴이기 때문에,createUser()
를 호출하고, 코루틴을 빠져나간다. (다른 스레드에서 실행)createUser()
가 완료되면 코루틴으로 들어와서 다음 함수를 호출한다.
- 비동기 작업을 순차적으로 작성할 수 있다.
suspend fun initializeSendbird() {
val result = createUser()
registerToken(result)
doSomething()
}
5. 동작방식 - S/W
- 코루틴은 어떻게 suspendable하게 동작하는가?
- 코루틴은 CPS(Conrinuation Passing Style)이라는 형태의 코드로 변환된다.
- label로 중단점을 나누고, 함수 실행 완료되면, resume을 호출하여 코루틴을 재개한다.
suspend fun myCoroutine(cont: MyContinuation) {
val userData = fetchUserData()
val userCache = cacheUserData(userData)
updateTextView(userCache)
}
⬇️
fun myCoroutine(cont: MyContinuation) {
when(cont.label) {
0 -> {
cont.label = 1
fetchUserData(cont)
}
1 -> {
val userData = cont.result
cont.label = 2
cacheUserData(userData, cont)
}
2 -> {
val userCache = cont.result
updateTextView(userCache)
}
}
}
fun fetchUserData(cont: MyContinuation) {
val result = "[서버에서 받은 사용자 정보]"
cont.resumeWith(Result.success(result))
}
출처
[Coroutine] Coroutine(코루틴)과 Subroutine(서브루틴)의 차이 - Coroutine이란 무슨 뜻일까?
알기쉬운 코루틴 이론::Android Studio에서 Kotlin으로#28
코틀린 코루틴(coroutine) 개념 익히기 · 쾌락코딩
'Programming > 언어' 카테고리의 다른 글
[C++] 선언과 정의는 다르다. (0) | 2024.07.06 |
---|---|
[Kotlin] Kotlin의 Null Safety. ? / ?. / ?: / !! 가 무엇일까? (1) | 2023.10.22 |
[Kotlin] Delegation (0) | 2023.03.01 |
[Kotlin] object, companion object (0) | 2023.02.26 |
[Kotlin] Generics 공변성, 반공변성(out, in) (0) | 2023.02.26 |