코루틴(Coroutine)은 비동기 프로그래밍을 간결하고 효율적으로 처리할 수 있게 해주는 프로그래밍 개념 중 하나입니다.
코틀린(Kotlin)에서는 코루틴을 통해 백그라운드 작업, 네트워크 통신, 데이터베이스 접근과 같은 비동기 작업을 쉽게 처리할 수 있습니다. 코루틴은 기존의 스레드 기반 프로그래밍보다 자원을 적게 사용하며, 코드를 더 읽기 쉽고 이해하기 쉬운 형태로 작성할 수 있게 돕습니다.
비동기 프로그래밍이란
간단히 말하자면 비동기 프로그래밍은 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 실행하는 프로그래밍 패러다임 즉, 작업이 끝나기를 기다리는 대신 다른 작업을 계속 처리할 수 있게 해줍니다. 이는 특히 I/O 작업, 네트워크 요청, 파일 시스템 작업 등의 경우 유용합니다.
Blocking / Non-blocking 과 Sync / Async 는 다른 개념이다.
Blocking / Non-blocking 과 Sync / Async 는 '관점'을 어떻게 두느냐에 따라 다르다고 할 수 있겠다.
뒤에서 더 자세히 알아보도록 하고 일단 각각의 개념에 대해 알아보자
동기(Synchronous): 동기 방식에서는 작업을 순차적으로 실행
하나의 작업이 완료된 후 다음 작업이 시작됩니다. 이는 작업의 완료를 기다리며, 해당 작업이 끝나야만 다음 작업으로 넘어갈 수 있음을 의미합니다. 동기 방식은 코드의 실행 순서가 예측 가능하고 이해하기 쉽지만, 작업 처리 중 시스템의 자원을 효율적으로 사용하지 못할 수 있습니다.
비동기(Asynchronous): 비동기 방식에서는 작업의 완료를 기다리지 않고 다음 작업을 바로 시작할 수 있습니다. 비동기 작업을 요청하고, 그 작업이 진행되는 동안 다른 작업을 수행할 수 있어 자원을 보다 효율적으로 사용할 수 있습니다. 비동기 방식은 동시에 여러 작업을 처리할 수 있지만, 작업의 완료 순서를 관리하고 결과를 동기화하는 것이 복잡할 수 있습니다.
동기와 블로킹, 비동기와 논블로킹이 유사한 개념 같은데 어떤 관점으로 이 개념을 구분할 수 있을까
Blocking / Non-blocking 은 호출된 함수가 호출한 함수에게 제어권을 바로 주느냐 안주느냐,
Sync / Async 는 호출된 함수의 종료를 호출한 함수가 처리하느냐, 호출된 함수가 처리하느냐의 차이다.
이해가 쉽지가 않다.
아래 영상에서 잘 설명해주셔서 예시를 보는게 이해가 빠를 것 같다.
https://www.youtube.com/watch?v=oEIoqGd-Sns
아래 그림 정도의 개념만 들고 예시로 보자
입력 요청이 일어났을 때 Blocking Sync 방식을 사용한다.
제어권이 호출된 함수로 넘어갔기 때문에 아래 내용은 실행이 되지 않는다. 일반적으로 우리가 아는 Sync의 개념일 것이다.
실제 안드로이드에서는 이런 입출력이 MainThread에서 일어나면 안되므로 Android에서는 동기 & 블로킹이 일어나는 일은 정상적인 앱에서는 없을 것으로 생각이된다.
게임에서 Map을 이동하는 예시로 들고 있습니다.
게임에서 새로운 Map을 로드할 때 진행 프로그래스를 표시하는데 이때 작업이 완료되었는지 주기적으로 확인하는게 동기 & 논블로킹의 예시 입니다.
호출자는 서버의 호출된 함수의 최신 상태를 주기적으로 알 수 있습니다.
하지만 단점으로 계속 확인하는 과정에서 오버헤드가 발생하고, 확인 간격 사이에 변경 상황이 즉시 감지되지 않는다는 단점들이 있습니다.
해당 조합은 동기 & 블로킹과 동일하게 동작되는 것 처럼 느껴집니다.
보통 이 조합의 경우 비동기 & 논블로킹 으로 하려다가 개발자의 실수로 이와 같이 동작하는 경우가 있다고 합니다. 일반적인 경우가 아니라고 합니다.
안드로이드에서 가장 흔히 사용되는 패턴입니다
예를 들어 Retrofit을 사용하여 네트워크 요청을 처리하는 경우가 있습니다.
import androidx.compose.runtime.*
import androidx.compose.material.*
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
@Composable
fun UserProfile(userId: String) {
val viewModel: UserProfileViewModel = viewModel()
val coroutineScope = rememberCoroutineScope()
val userProfileState = viewModel.userProfile.collectAsState()
LaunchedEffect(key1 = userId) {
coroutineScope.launch {
viewModel.loadUserProfile(userId)
}
}
/**
기타 UI
**/
when (val state = userProfileState.value) {
is UserProfileState.Loading -> CircularProgressIndicator()
is UserProfileState.Success -> Text("User Name: ${state.userProfile.name}")
is UserProfileState.Error -> Text("Error: ${state.error}")
}
}
해당 부분에서 loadUserProfile 을 통해 유저의 프로필 정보를 요청합니다.
이는 NonBlocking으로 바로 요청만하고 기타 ui작업을 이어서 실행합니다.
또한 함수의 종료 또한 호출된 함수에서 관리하지 않고 알아서 종료하고 데이터를 ViewModel 내부 데이터에 저장합니다.
이 방식은 비동기 작업을 논블로킹으로 처리하며, 작업의 결과에 따라 UI를 동적으로 업데이트합니다.
제 개인적으로 실제 안드로이드에서는 이러한 것들을 모두 자세히 알 필요는 없다고 생각합니다.
그냥 이 정도 개념있구나 인지하고 자신의 의도대로 올바르게 동작이 되고 있는지 그것을 판단할 때 도움 되는 개념정도라고 생각합니다.
기존에는 쓰레드 방식을 통해 비동기 프로그래밍을 주로 하였습니다.
하지만 어떠한 이유로 인해 코루틴이 나오게 되었고 많은 사람들이 사용하고 있는걸까요?
1. 스레드 생성과 컨텍스트 스위칭 비용
스레드는 운영 체제에 의해 관리되는 독립적인 실행 단위입니다. 각 스레드에는 고유한 호출 스택이 할당되며, 이는 스레드가 함수를 호출하고 지역 변수를 저장하는 데 사용됩니다. 여러 스레드가 동시에 실행될 때, 운영 체제는 스레드 간의 컨텍스트 스위칭을 수행해야 합니다. 이는 현재 실행 중인 스레드의 상태를 저장하고, 다음에 실행할 스레드의 상태를 불러오는 과정을 말합니다. 컨텍스트 스위칭은 상당한 CPU 자원을 소모하는 작업으로, 특히 많은 수의 스레드가 동시에 실행될 때 이 비용은 더욱 증가합니다.
반면, 코루틴은 기본적으로 단일 스레드 내에서 실행됩니다. 코루틴은 작업 간 전환 시 운영 체제 레벨의 컨텍스트 스위칭을 필요로 하지 않습니다. 대신 코루틴 간 전환은 코루틴 객체의 상태 변경으로 처리됩니다. 코루틴이 일시 중단되면, 그 상태(예: 지역 변수, 실행 지점)는 힙 메모리에 저장되고, 이후 재개될 때 이 상태 정보를 바탕으로 실행이 계속됩니다. 이 과정에서 발생하는 오버헤드는 운영 체제가 관리하는 스레드의 컨텍스트 스위칭에 비해 훨씬 작습니다.
요약하면, 스레드는 OS의 커널에 의해 관리되며 컨텍스트 스위칭으로 인한 비용이 발생하는 반면, 코루틴은 하나의 스레드에서 작업 객체의 상태 전환만으로 동시성을 처리하여 자원 사용을 최소화하는 효율적인 방법을 제공합니다
작업의 상태 종류
1. 실행 중(Active): 코루틴이 실행 중이며 작업을 수행하고 있는 상태입니다.
2. 일시 중단(Suspended): 코루틴이 I/O 작업이나 네트워크 호출 등을 기다리며 실행을 일시적으로 멈춘 상태입니다. 이 상태에서 코루틴은 CPU 자원을 소비하지 않습니다.
3. 완료(Completed): 코루틴의 작업이 완료된 상태입니다.
이게 Coroutine
을 경량 쓰레드(Lightweight Thread)라고 부르는 이유다.
2. 자원 효율성
코루틴이 CPU 자원을 보다 효율적으로 사용할 수 있는 핵심 이유는 스레드를 사용하는 전통적인 방식에 비해 더 작은 작업 단위로 실행을 세분화할 수 있기 때문입니다. 스레드가 대기 상태에 들어가면, 그 스레드는 실행 가능한 다른 작업이 있음에도 불구하고 CPU 자원을 활용하지 못하고 메모리 자원을 소모하면서 대기합니다.
반면 코루틴은 단일 스레드 내에서 여러 '경량' 작업(코루틴)을 수행할 수 있게 합니다. 코루틴은 서로 협력적으로 작업을 수행하며, 하나의 코루틴이 I/O 작업이나 다른 대기 조건으로 인해 일시 중지되어도, 그 스레드는 멈추지 않고 다른 코루틴의 작업을 계속 처리할 수 있습니다.
그 외에도 코드적인 부분에서도 여러 장점이 있다고 하는데 저는 스레드를 많이 사용해보지 않아서 딱히 와닿지 않네요.
fun main() = runBlocking {
launch {
wait1000ms()
println("World!")
}
println("Hello")
}
suspend fun wait1000ms() {
delay(1000L)
}
이 코드는 runBlocking 코루틴 빌더를 사용하여 메인 스레드를 블로킹하고, 그 내부에서 launch 코루틴 빌더를 통해 새로운 코루틴을 시작합니다.
launch로 시작된 코루틴 내에서 wait1000ms 함수는 delay 함수를 호출하여 코루틴을 1초 동안 중단시킵니다.
이때, delay 함수는 중단 함수(suspending function)이기 때문에 코루틴의 실행을 멈추게 하지만, 스레드를 차단하지는 않습니다.
따라서, println("Hello") 구문은 launch 코루틴 블록이 완료되기를 기다리지 않고 즉시 실행되어 "Hello"를 출력합니다. 1초 후, wait1000ms 함수 내의 delay가 완료되고, println("World!")가 실행되어 "World!"를 출력합니다.
코루틴 빌더는 코루틴을 시작하기 위한 함수입니다. 코루틴 스코프 내에서 코루틴을 생성하고 시작하는 역할을 합니다.
코루틴 빌더에는 launch
와 async
가 있습니다.
launch: 반환 값이 없는 작업을 비동기적으로 실행할 때 사용됩니다. launch
는 Job
객체**를 반환하며, 이는 코루틴의 실행을 제어할 수 있게 해줍니다.
async: 반환 값이 있는 작업을 비동기적으로 실행할 때 사용됩니다. async
는 Deferred<T>
객체를 반환하며, 이는 나중에 결과 값을 가져올 수 있게 해줍니다.
val deferredResult: Deferred<Type> = GlobalScope.async {
// 비동기 작업 수행 후 결과 반환
}
val result: Type = deferredResult.await() // 결과값 기다리기
코루틴 빌더는 코루틴을 시작하기 위한 함수이고 코루틴은 빌더에 의해 시작되는 실행 흐름
코루틴 빌더 내부에는 여러 개의 코루틴이 있을 수 있습니다
즉, 코루틴은 자식 코루틴을 가질 수 있으며, 이는 코루틴 계층 구조의 중요한 부분입니다.
코루틴 스코프 내에서 시작된 코루틴은 자동으로 해당 스코프에 속한 부모 코루틴의 자식 코루틴이 됩니다.
부모 자식 관계의 동작 또한 중요한데 뒤에서 필요할 때 더 알아보겠습니다.
코루틴은 한 스레드 내에서 기본적으로 동시적
코루틴은 기본적으로 선점 x
suspend function
은 코틀린 코루틴에서 비동기 코드를 간결하고 효율적으로 작성하기 위한 핵심 개념 중 하나
suspend function
은 네트워크 요청, 데이터베이스 작업 등 비동기적으로 수행되어야 하는 작업을 처리하는 데 주로 사용됩니다. 함수의 실행을 일시 중지했다가, 비동기 작업의 결과가 준비되면 자동으로 재개합니다. 이 과정은 코루틴 스케줄러에 의해 관리됩니다.
suspend function
을 사용함으로써 복잡한 비동기 로직도 마치 동기 코드를 작성하듯이 간결하고 이해하기 쉽게 표현할 수 있습니다.
네트워크 호출을 순차적으로 여러 번 실행해야 하는 상황을 가정해 봅시다. 코루틴을 사용하지 않고 콜백을 이용해 이를 구현한 코드는 다음과 같을 수 있습니다:
// Callback을 사용한 예시 (코루틴 사용 X)
fun fetchData(callback: (String) -> Unit) {
// 네트워크 요청을 가정한 비동기 작업
Thread {
Thread.sleep(1000) // 대기를 시뮬레이션
callback("Data fetched")
}.start()
}
fun main() {
fetchData { result1 ->
println(result1)
fetchData { result2 ->
println(result2)
fetchData { result3 ->
println(result3)
// 추가적인 네트워크 요청이 필요하다면 이런 식으로 계속됩니다.
}
}
}
}
이 코드는 네트워크 요청을 가정한 비동기 작업을 순차적으로 수행합니다. 콜백 지옥(callback hell)이라 불리는 이 패턴은 중첩이 깊어질수록 코드의 가독성과 유지보수성이 크게 떨어집니다.
import kotlinx.coroutines.*
suspend fun fetchData(): String {
delay(1000) // 네트워크 요청을 가정한 비동기 작업
return "Data fetched"
}
fun main() = runBlocking {
val result1 = fetchData() // 첫 번째 요청
println(result1)
val result2 = fetchData() // 두 번째 요청
println(result2)
val result3 = fetchData() // 세 번째 요청
println(result3)
// 추가적인 네트워크 요청을 쉽게 추가할 수 있습니다.
}
위 코드의 경우 fetchData가 순차적으로 실행됩니다.
suspend
함수를 순차적으로 호출하는 것은 비효율적입니다.
논 블로킹 실행을 통해 성능을 향상시킬 수 있으며, async
와 await
을 사용하여 동시에 여러 API 호출을 실행할 수 있습니다.
즉 하나의 코루틴 내에서는 동기적이기에 여러 코루틴을 만들면 됌
import kotlinx.coroutines.*
suspend fun fetchData(): String {
delay(1000) // 네트워크 요청을 가정한 비동기 작업
return "Data fetched"
}
fun main() = runBlocking {
// 병렬로 fetchData() 실행
val deferredResult1 = async { fetchData() } // 첫 번째 요청
val deferredResult2 = async { fetchData() } // 두 번째 요청
val deferredResult3 = async { fetchData() } // 세 번째 요청
// 결과 기다리기
println(deferredResult1.await())
println(deferredResult2.await())
println(deferredResult3.await())
// 추가적인 네트워크 요청도 같은 방식으로 쉽게 추가할 수 있습니다.
}
정리
비동기 작업을 수행할 때 예제와 같이 작업들의 상관관계와 범위를 나눠서 생각을 해야함
부모 자식 관계 : 어떤 작업이 내부적으로 어떤 작업들로 이루어져 있고
작업 의존 관계 : 어떤 작업이 끝나야 그 다음 작업을 진행하는지
코루틴은 기본적으로 Coroutine Scope이라는 작업 범위를 만드는 것으로 시작
Coroutine Scope은 이름에서 유추할 수 있듯이 코루틴 작업의 범위(Scope)를 의미함
코루틴 스코프(Coroutine Scope)는 코루틴이 실행될 수 있는 컨텍스트를 정의합니다. 코루틴 스코프는 코루틴이 어떻게, 언제 실행될지를 결정하며, 코루틴의 생명 주기를 관리합니다. 코틀린에서는 다양한 코루틴 스코프를 제공하며, 각각의 사용 목적과 환경에 맞게 선택하여 사용할 수 있습니다.
코루틴 내부 많은 API 및 중단 함수는 CoroutineScope 안에서만 사용 가능하고, 밖에서는 컴파일 에러로 사용 불가
빌더들은 해당 스코프에 종속된 코루틴을 생성하며, 스코프가 취소되면 모든 하위 코루틴도 자동으로 취소
정리하면코루틴 스코프는 실행 범위를 정의하며, 코루틴의 생명주기를 관리한다 이로써 Structured concurrency(구조화된 동기성)을 제공함
구조화된 동기성(Structured Concurrency)란 코루틴과 같은 비동기 프로그래밍을 더 안전하고 이해하기 쉽게 만들기 위한 프로그래밍 패러다임
모든 비동기 작업이 명확하게 정의된 범위 내에서 실행되어야 하며, 그 범위가 종료될 때 모든 작업이 완료되거나 취소되어야 한다는 것입니다. 이로 인해 코드의 실행 흐름을 더 명확하게 파악할 수 있고, 메모리 누수 및 관련된 리소스 관리 문제를 방지할 수 있습니다.
코루틴은 Scope에 바인딩 되며 부모 Scope이 종료 or 취소되면 자동으로 내부의 모든 코루틴 역시 종료 or 취소 됨
ex) ViewModel이 종료되면 viewModelScope 이 종료되고 내부에 바인딩된 모든 코루틴이 종료 됨
Coroutine Scope에 바인딩 되는 실행 환경 정보
코루틴의 이름, 디스패처, 잡 등 실행에 필요한 정보들을 보유
주요 Context : Coroutine Name, Job, Dispatcher
Coroutine Scope 내부에 Coroutine Context가 존재한다.
컨텍스트 정보들은 링크드 리스트 형태로 연결되어 있음
Context를 추가하고 싶을 때는 + 연산이 정의되어 있어서 Context A + Context B 이런식으로 사용 가능하다. Set과 같이 한 타입은 리스트에서 고유해야 함 ex) CoroutineName을 중복해서 추가하면 뒤에 넣은 이름이 덮어 씀
import kotlinx.coroutines.*
fun main() = runBlocking { // 이곳이 최상위 코루틴 스코프입니다.
launch {
delay(1000L)
println("코루틴 1 실행")
}
launch {
delay(500L)
println("코루틴 2 실행")
}
println("모든 코루틴 시작됨")
}
이 말은 한 스코프에서 생성된 코루틴들은 runBlocking의 컨텍스트를 상속받습니다
내부에 코루틴을 생성할 때 launch나 async를 사용하여 필요에 따라 자신만의 컨텍스트를 추가로 구성할 수 있습니다.
부모 코루틴 스코프가 자식의 Job 들을 컨텍스트로 관리하고 있으므로 쉽게 취소를 전파할 수 있습니다.
Coroutine Scope은 기본적으로 시스템이 제공해주는 미리 정의된 Scope과 유저가 직접 생성해서 사용하는 커스텀 Scope 두 종류가 있음
미리 정의된 Scope
1. GlobalScope : 앱의 생명주기와 함께 하는 Scope (잘 사용 x)
2. lifecycleScope : Android에서 컴포넌트의 라이프사이클에 해당하는 Scope
3. viewModelScope : Android에서 ViewModel의 라이프사이클에 해당하는 Scope
커스텀 Scope
CoroutineScope 함수를 이용해서 직접 생성
커스텀 CoroutineScope를 생성할 때는 코루틴 컨텍스트를 인자로 제공해야 합니다.
fun main() {
val customScope = CoroutineScope(Dispatchers.Default + Job())
customScope.launch {
// 커스텀 스코프 내부에서 코루틴 실행
println("코루틴 실행 중...")
}
// 필요한 경우 스코프의 생명주기를 관리할 수 있음
// customScope.cancel()을 호출하여 스코프와 모든 코루틴을 취소할 수 있음
}
코루틴 디스패처(Coroutine Dispatcher)는 코루틴이 어떤 스레드에서 실행될지 결정하는 역할을 합니다. 코틀린 코루틴에서는 Dispatcher 객체를 통해 여러 가지 디스패처를 제공하며, 각 디스패처는 코루틴이 실행될 컨텍스트를 결정합니다.
Dispatchers.Default
와 마찬가지로 백그라운드 스레드 풀을 사용하지만, I/O 작업의 특성을 고려하여 구성되어 있습니다.
fun main() = runBlocking {
launch(Dispatchers.Main) {
// UI 업데이트 작업
}
launch(Dispatchers.IO) {
// 네트워크 호출 등 I/O 작업
}
launch(Dispatchers.Default) {
// CPU 집약적 작업
}
}
코루틴에서 디스패처를 명시적으로 지정하지 않으면, 코루틴이 실행될 기본 디스패처는 상위 코루틴의 컨텍스트를 상속받습니다.
코루틴을 이용해서 논블로킹 비동기적으로 작업을하는 방법
아래 코드는 runBlocking에서 시작해서 각각의 코루틴 하나씩 시작하겠다고 알리고 나오는 것이다.
그럼 각각의 코루틴은 논블로킹 비동기적으로 수행하지만 이는 동시성인 것이지 병렬성은 아니다.
각각의 코루틴 내부에서는 동기적으로 수행된다
이때 주의할점은 각 코루틴은 협조적이여야한다. 즉, 끝날 수 있어야하고 작업이 매우 긴 경우 중간중간에 중지함수가 포함되어있어야한다
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("m s")
launch {
val user = loadUser()
println("user: $user")
}
launch {
val product = loadProduct()
println("product: $product")
}
println("m f")
}
}
suspend fun loadUser() : String {
println("start use")
delay(2000)
println("finish use")
return "user"
}
suspend fun loadProduct() : String {
println("start pro")
delay(1500)
println("finish pro")
return "product"
}
아래와 같이 async & wait를 쓰는 코드는 어떨까
fun main() {
runBlocking {
println("main started")
val user = async { loadUser() }
println("user loading request")
val product = async { loadProduct() }
println("product loading request")
println("user : ${user.await()}")
println("product : ${product.await()}")
println("main finished")
}
}
suspend fun loadUser(): String {
println("start use")
delay(2000)
println("finish use")
return "user"
}
suspend fun loadProduct(): String {
println("start pro")
delay(1500)
println("finish pro")
return "product"
}
[result]
main started
user loading request
product loading request
start use
start pro
finish pro
finish use
user : user
product : product
main finished
코루틴은 순서대로 시행이 된다.
하지만 첫번째 코루틴에서 두번째 코루틴(user)이 종료될때까지 기다리는 await를 쓴다.
그 와중 세번째(product) 코루틴이 끝나지만 첫번째 코루틴은 아직 두번째 코루틴의 종료를 기다리므로 println("product : ${product.await()}") 는 실행이 안된다.
withContext
는 코루틴 내에서 다른 디스패처(즉, 다른 스레드 또는 스레드 풀)로 코드 블록의 실행 컨텍스트를 임시적으로 전환할 때 사용하는 코틀린 코루틴 라이브러리의 함수입니다.
withContext
를 사용하면 현재 코루틴의 실행을 잠시 중단하고, 지정한 디스패처에서 코드 블록을 실행한 후, 결과를 반환하고 원래의 컨텍스트로 돌아올 수 있습니다.
withContext는 엄밀히 말하면 코루틴 빌더가 아니다
따라서 중간에 다른 작업을 디스패처를 변경하여 동시적으로 실행 하고 싶으면 async 또는 launch를 사용하면 된다.
작업은 때때로 취소가 가능해야 함
ex) 화면을 종료해서 더이상 API로 부터 데이터를 받아올 필요가 없는 경우
ex) 시간이 비정상적으로 오래 걸려서 작업을 진행할 수 없는 경우
취소를 하지 않으면 사용하지 않는 메모리와 CPU의 낭비가 있음
코루틴의 실행 취소는 CoroutineContext의 한 종류인 Job이 관리
Job은 이전에 설명했듯이 부모와 자식이 있어서 구조적 동시성을 지원해줌
이러한 구조를 통해, 부모 Job이 취소되거나 실패할 경우, 그에 속한 자식 Job들도 함께 취소되거나 실패 처리되는 등의 동작을 자동으로 수행할 수 있습니다.
또한 부모 코루틴이 완료되기 전에는 자식 코루틴들이 모두 완료되어야 합니다. 이는 코드의 실행 흐름을 더욱 안전하고 예측 가능하게 만듭니다
1. launch에서 코루틴 취소
launch의 경우 코루틴을 만들면서 해당하는 Job을 리턴, 이 때 얻은 Job을 통해서 취소 가능
2. async에서 코루틴 취소
async의 경우 코루틴을 만들면서 해당하는 Deferred를 리턴
Deferred는 Job의 자식 인터페이스
Deferred를 통해서 취소 가능
await() 중에 취소되면 해당 지점에서 취소 에러 발생(JobCancellationException)
따라서, await
를 호출할 때는 코루틴의 취소 상태를 고려하거나, 적절한 예외 처리를 구현해야 합니다.
try {
deferred.await() // 코루틴이 취소된 경우 CancellationException 발생
} catch (e: CancellationException) {
println("Caught CancellationException")
}
3. CoroutineScope에서 코루틴 취소
CoroutineScope 자체를 cancle 할수도 있습니다. 이 경우 정의된 Scope의 모든 코루틴을 취소 시킬 수 있음 Scope은 자신 내부에 등록된 Job을 탐색해서 취소를 요청
4. viewModelScope에서 코루틴 취소
viewModelScope
는 Jetpack ViewModel 컴포넌트에 속한 프로퍼티로, ViewModel이 소유하는 코루틴 스코프입니다.
이 스코프는 ViewModel의 생명주기에 바인딩되어 있어, ViewModel이 소멸될 때 자동으로 스코프 내의 모든 코루틴이 취소됩니다. 이 또한 구조적 동시성이죠
부모가 취소되면 자식에게도 취소가 전파 됨
반대로 자식이 취소되더라도 부모는 영향을 받지 않음
부모 취소
fun main() {
runBlocking {
val parentJob = launch {
println("parent start")
launch {
println("child start")
for (i in 0 .. 10) {
delay(100)
println("i : $i")
}
println("child complete")
}
delay(1000)
println("parent complete")
}
delay(500)
parentJob.cancel()
}
}
[result]
parent start
child start
i : 0
i : 1
i : 2
i : 3
자식 취소
fun main() {
runBlocking {
val parentJob = launch {
println("parent start")
val child = launch {
println("child start")
for (i in 0 .. 10) {
delay(100)
println("i : $i")
}
println("child complete")
}
delay(300)
child.cancel()
println("parent complete")
}
delay(500)
}
}
[result]
parent start
child start
i : 0
i : 1
parent complete
개발 할 때 종종 사용 후 자원을 해제해야 하는 경우가 있음
코루틴의 취소 자체는 코루틴이 실행 중인 작업을 중단시키지만, 자동으로 열린 파일이나 소켓 연결을 닫아주지는 않습니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
delay(1000
} finally
println("자원 정리")
}
}
delay(500)
job.cancelAndJoin()
println("코루틴 취소")
}
취소가 요청되면 코루틴이 중단 되는 시점에 CancellationException이 발생함
예외 처리로 finally 부분에 자원을 해제하면 됨
코루틴이 취소될 때 finally
블록이 실행되는 것은 보장되지만, 취소된 코루틴에서 일부 종류의 작업을 수행하려고 하면 CancellationException
이 발생할 수 있습니다. 특히, suspend 함수를 finally 블록 내에서 호출하려고 할 때 주의가 필요
finally
블록을 사용하여 자원 정리 코드를 구현할 때, 다음 사항을 고려해야 합니다:
finally
블록 내에서는 취소에 안전한, 즉 블로킹되지 않는 작업을 수행해야 합니다.NonCancellable
컨텍스트 사용: suspend
함수를 포함한 중요한 정리 작업을 수행해야 하는 경우 withContext(NonCancellable)
을 사용합니다.NonCancellable
은 코루틴의 취소 상태와 무관하게 작업을 수행하기 위해 사용되는 코루틴 컨텍스트입니다.
예를 들어 자원 해제나 정리 로직과 같은 경우에 유용합니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
try {
delay(1000)
} finally {
withContext(NonCancellable) {
println("자원 정리 코드")
delay(1000)
println("자원 정리 완료")
}
}
}
delay(500)
job.cancelAndJoin()
println("코루틴 취소")
}
import kotlinx.coroutines.*
import java.io.File
fun main() = runBlocking {
val job = launch {
File("example.txt").inputStream().use { inputStream ->
// 파일 처리
println("파일 읽는 중...")
delay(1000)
}
}
delay(500)
job.cancelAndJoin()
println("코루틴 취소 완료")
}
Closable 인터페이스가 구현된 자원이라면 use {} 를 통해서 내부적으로 finally { close() } 호출이 가능함 try finally { close() } 와 원리는 똑같지만 이를 좀 더 쉽게 쓸 수 있도록 지원해주는 API
그 외에도 여러가지가 있는데 그냥 try,finally와 withContext(NonCancellable)을 사용하는게 가장 직관적이면서 효과적일듯하다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
while (true) {
println("in progress")
}
}
delay(500)
job.cancel()
}
위 코드를 보자
위 코드는 영원히 끝나지 않는다.
이상한 점을 느낄것이다.
새로운 코루틴을 만들어서 논블로킹 비동기적으로 동작하는데 왜 cancle이 되지않을까
그 이유는 취소 요청 즉시 코루틴이 종료되는 것은 아니기 때문이다.
job.cancel()
을 호출하면 코루틴에 취소 신호가 전송됩니다. 하지만, 코드 내의 while (true)
루프는 취소 체크를 수행하지 않기 때문에, 코루틴이 취소되어도 무한 루프는 계속 실행됩니다.
코루틴의 취소는 "협조적"이어야 하며, 코루틴이 실행 중인 작업은 주기적으로 취소 여부를 확인하고 취소 상태에 따라 적절히 동작을 중단해야 합니다.
delay(), yield()와 같이 코루틴이 중단되는 시점에 도달하면 코루틴이 취소되었는지 확인하게 됨
만약 중단점 없이 연산작업을 오래 해야 한다면 해당 작업은 끝날 때까지 취소가 불가능함
fun main() = runBlocking {
val job = launch {
while (true) {
println("in progress")
yield()
}
}
delay(500)
job.cancel()
}
yield
는 코루틴이 긴 작업을 수행하고 있을 때, 다른 코루틴도 실행할 기회를 가질 수 있도록 합니다. 이는 코틀린 코루틴에서 협력적 멀티태스킹을 지원하는 중요한 메커니즘 중 하나입니다.
즉 runBlocking 부분이 중지를 해주어서 job이 실행되는데 job은 중지함수 없이 완전히 선점하고 있기 때문에 해당 부분도 중지함수를 포함시켜 협조적으로 만들어줘야한다.
마치 데드락에 걸린듯한 느낌을 준다.
이는 중지함수를 통해 해결가능
글이 너무 길어진거 같아서 다음 포스팅에 이어서...