코루틴의 실행 환경을 설정하고 관리하는 인터페이스
즉, 코루틴의 실행 환경을 정의하는 데이터 구조라고 말할 수 있습니다.
CoroutineContext는 CoroutineName, Dispatcher, Job, CoroutineExceptionHandler 로 구성되고 각 구성요소에 대해 알아보겠습니다.
코루틴에 이름을 부여하여 디버깅을 용이하게 합니다.
private fun main() = runBlocking<Unit> {
launch(context = CoroutineName("Coroutine1")) {
delay(500L)
println("[${Thread.currentThread().name}] ${this.coroutineContext[CoroutineName]}")
}
launch(context = CoroutineName("Coroutine2")) {
delay(500L)
println("[${Thread.currentThread().name}] ${this.coroutineContext[CoroutineName]}")
}
}
//출력 [main] CoroutineName(Coroutine1)
//출력 [main] CoroutineName(Coroutine2)
위의 코드를 실행시켜보면 main 쓰레드에 코루틴 이름을 직접 설정한 "Coroutine1", "Coroutine2"로 나오는 것을 확인할 수 있습니다.
코루틴이 실행될 스레드를 결정합니다(코루틴은 '중단 및 재개' 되므로 스레드가 필요함).
개발자가 직접 정의한 Dispatcher나 미리 정의된 Dispatcher를 사용하여 작업이 어느 스레드풀에서 동작할 것인지 결정할 수 있습니다.
@OptIn(DelicateCoroutinesApi::class)
private fun main() = runBlocking<Unit>{
println("[${Thread.currentThread().name}]실행")
// 멀티 스레드 디스패처 만들기
val multiThreadDispatcher: CoroutineDispatcher =
newFixedThreadPoolContext(nThreads = 2, name = "MultiiThread")
launch(multiThreadDispatcher + CoroutineName("Coroutine1")) {
println("[${Thread.currentThread().name}]실행, ${coroutineContext[CoroutineName]}")
}
launch(context = multiThreadDispatcher + CoroutineName("Coroutine2")) {
println("[${Thread.currentThread().name}]실행, ${coroutineContext[CoroutineName]}")
}
}
[main]실행
[MultiiThread-1]실행, CoroutineName(Coroutine1)
[MultiiThread-2]실행, CoroutineName(Coroutine2)
위의 코드를 실행시키면 직접 스레드풀(multiThreadDispatcher)을 정의했습니다.
launch함수의 인자로 선언한 Dispatcher를 넘기고 디버깅을 위해 + CoroutineName을 설정했습니다.
그러나 개발자가 직접 Dispatcher 객체를 만들어 사용하면 비효율적으로 동작할 가능성이 높습니다.
스레드의 생성 비용은 매우 비싸며 협업 시 미리 만들어둔 디스패처의 존재를 몰라 다시 만들게 될 수도 있습니다.
이 문제를 막기 위해 코루틴 라이브러리에서 미리 정의된 CoroutineDispatcher가 존재합니다.
네트워크 요청, 파일 입출력 등의 I/O 작업을 위한 CoroutineDispatcher
context 객체에 Dispatchers.IO 싱글톤 인스턴스를 넘겨서 사용한다
@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking<Unit> {
launch(context = Dispatchers.IO + CoroutineName("IOCoroutine")) {
delay(500L)
println("Thread: ${Thread.currentThread().name}")
println("Dispatcher: ${coroutineContext[CoroutineDispatcher]}")
println("Coroutine Name: ${coroutineContext[CoroutineName]?.name}")
}
}
Thread: DefaultDispatcher-worker-1
Dispatcher: Dispatchers.IO
Coroutine Name: IOCoroutine
코루틴이 실행된 스레드의 이름이 DefaultDispatcher-worker-1임을 확인할 수 있습니다.
이는 코루틴에서 제공하는 공유 스레드풀에 속한 스레드의 이름입니다.
CPU 바운드 작업이 필요할 때 사용하는 CoroutineDispatcher.
CPU 연산이 많이 필요할 경우 context 객체에 Dispatchers.Default를 전달한다.
@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking<Unit> {
launch(context = Dispatchers.Default + CoroutineName("DefaultCoroutine")) {
delay(500L)
println("Thread: ${Thread.currentThread().name}")
println("Dispatcher: ${coroutineContext[CoroutineDispatcher]}")
println("Coroutine Name: ${coroutineContext[CoroutineName]?.name}")
}
}
Thread: DefaultDispatcher-worker-1
Dispatcher: Dispatchers.Default
Coroutine Name: DefaultCoroutine
Dispatchers.IO와 같이 Thread의 이름이 같다.
이는 IO와 Default는 같은 스레드풀을 사용한다는 것을 의미한다.
스레드풀 내 IO와 Default가 사용하는 스레드는 구분되지만 공유 스레드풀을 사용한다.
메인(UI) 스레드에서 실행
메인 스레드의 사용을 위해 사용되는 CoroutineDispatcher 객체
Job 객체는 코루틴을 추상화한 것으로 모든 코루틴 빌더 함수는 Job객체를 반환한다.
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
Job 객체는 코루틴의 시작, 완료, 취소 등 상태를 추적하며 코루틴을 제어할 수 있는 기능을 제공합니다.
모든 코루틴은 Job을 통해 관리되며, 이를 통해 코루틴의 상태를 모니터링 및 취소할 수 있습니다.
Job 객체에 cancel 함수를 사용하여 코루틴을 취소할 수 있습니다.
private fun main() = runBlocking<Unit> {
val longJob: Job = launch(Dispatchers.Default) {
repeat(100) {
delay(1000L)
println("반복횟수${it}")
}
}
delay(5000L)
longJob.cancel()
}
반복횟수0
반복횟수1
반복횟수2
반복횟수3
join(), joinAll()을 사용하여 코루틴을 순차처리할 수 있다.
private fun main() = runBlocking<Unit> {
val waitJob: Job = launch {
delay(500L)
println("나 끝날 때 까지 기다려")
}
waitJob.join()
print("언제 끝나냐..")
}
나 끝날 때 까지 기다려
언제 끝나냐..
private fun main() = runBlocking<Unit> {
val waitJob: Job = launch {
delay(500L)
println("나 끝날 때 까지 기다려")
}
val waitJob2: Job = launch {
delay(1000L)
println("두 번째도 기다려!")
}
joinAll(waitJob, waitJob2)
print("언제 끝나냐..")
}
나 끝날 때 까지 기다려
두 번째도 기다려!
언제 끝나냐..
isActive: 코루틴이 활성화돼 있는지의 여부. 활성화 상태라면 true, 그렇지 않다면 false
isCancelled: 코루틴이 취소 요청됐는지의 여부
isCompleted: 코루틴 실행이 완료됐는지의 여부. 코루틴의 모든 코드가 실행 완료되거나 취소 완료되면 true를 반환
코루틴 내에서 발생한 예외를 처리하는 역활
예외가 발생했을 때의 동작을 정의
private fun main() = runBlocking<Unit> {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
println("${coroutineContext[CoroutineName]}에서 예외 발생 $throwable")
}
CoroutineScope(CoroutineName("Coroutine1") + exceptionHandler).launch {
throw Exception("예외 발생")
}
delay(1000L)
}
주의할 점
만약 자식 코루틴이 부모 코루틴으로 예외를 전파하면 자식 코루틴에서는 예외가 처리된 것으로 봐 자식 코루틴에 설정된 Handler 객체는 동작하지 않는다
구조화된 코루틴상에 여러 CoroutineExceptionHandler 객체가 설정돼 있더라도 마지막으로 예외를 전파받는 위치에 설정된 CoroutineExceptionHandler 객체만
예외를 처리한다