CoroutineContext

KwangYoung Kim·2024년 7월 19일
0
post-thumbnail

Coroutine에서 Context는 코루틴이 실행될 때의 조건이나 환경등의 정의이다.
여러 요소들로 이루어질 수 있으며 우리가 자주 사용하는 Job또한 CoroutineContext를 구현하고 있다.

대표적으로 CoroutineContext는 코루틴이 실행될 스레드 또는 스레드풀을 지정할 수 있는 Dispatcher가 있고 에러를 핸들링할 수 있는 CoroutineExceptionHandler 등이 있다.

Dispatchers

Dispatchers는 코루틴이 실행될 스레드나 스레드풀을 지정해준다.
각 스레드 상황에 맞게 작업을 분배하고
launch, async 와 같은 코루틴 빌더에 인자로 Dispatchers를 넣어 실행될 스레드를 지정해줄 수 있다.
넣지 않으면 실행한 Scope의 Dispatchers를 상속받아 사용하게 된다.

대표적인 Dispatchers부터 알아보자.

Dispatchers.Main

실행되고 있는 어플리케이션 메인 스레드에서 동작함을 보장해준다.(단일 스레드)
이 Dispatchers를 사용하는 경우는 보통 UI에 관련된 작업을 할 때 사용한다.

💡 위 내용은 안드로이드 기준으로 설명한 것이기 때문에 만약 서버 개발이라던지 다른 프로젝트에서는 UI스레드가 아닐 수 있다.

Dispatchers.Default

만약 Dispatcher 설정이 없다면 기본적으로 설정되어 있는 Dispatcher이다.
이 Dispatcher에서 이용할 수 있는 스레드 개수는 최소 2개이고 최대 실행되는 디바이스의 CPU 코어 숫자(정확히는 스레드 수)만큼 사용한다.
스레드를 block하지 않고 계속해서 작업하는 정렬 등 CPU를 이용한 연산작업에 유리하다.
만약 코어의 개수보다 스레드를 많이 사용하게 된다면 컨텍스트 스위칭으로 인한 오버헤드가 발생하기 때문에 CPU 코어 숫자만큼만 스레드를 사용하는 것 같다.

Dispatchers.IO

최대 64개(또는 코어 수 중 많은 쪽)의 스레드를 사용하며 파일 입출력, 네트워크 작업 등 스레드를 block 시키는 작업에 유리하다.
네트워크 작업이나 DB, 파일 입출력 등 스레드가 대기해야하는 상황이 오면 컨텍스트 스위칭을 통해 작업을 하지 않는 스레드로 이동하여 작업을 하면 효율적이기 때문에 Dispatcher.Default 보다 많은 스레드를 사용한다.

Dispatchers.Unconfined

첫번째 중단(suspend) 지점까지는 실행한 Context에서 재개되다가 중단지점 이후 중단지점에서 사용한 스레드에서 재개된다.
delay 함수의 경우 DefaultExecutor에서 실행된다.

launch(Dispatchers.Unconfined) {
    println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
    delay(500)
    println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
}

launch { 
    println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
    delay(1000)
    println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
}  
    
실행결과

Unconfined      : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main

Error Handling

CoroutineExceptionHandler

코루틴에서 전파되는 예외를 잡아 처리할 수 있는 Context이다.
asyncwithContext 에서는 exception을 잡지 못한다.

val exceptionHandler = CoroutineExceptionHandler { _, exception ->
    println("Exception is $exception")
}

CoroutineScope(Dispatchers.Default + exceptionHandler).launch {
    throw IllegalStateException("can't do")
}

실행결과

Exception is java.lang.IllegalStateException: can't do

SupervisorJob

Job은 CoroutineContext를 구현한 Element라는 interface를 구현한다.
즉 Job 또한 CoroutineContext이다.

SupervisorJob은 하위 코루틴에서 발생한 예외를 상위 코루틴으로 전파시키지 않아 다른 하위 코루틴마저 취소되는 경우를 막아줄 수 있다.
아래 예시 코드 같은 경우 첫 번째 하위 코루틴은 "First coroutine before work" 까지 실행되고 에러가 발생되서 취소된다.
하지만 두번째 코루틴은 "Second coroutine work"를 출력하고 전체 코루틴은 정상 종료된다.

CoroutineScope(Dispatchers.Default).launch {
    launch(SupervisorJob()) {
        println("First coroutine before work")
        error("Error")
        println("First coroutine after work")
    }

    launch {
        delay(100)
        println("Second coroutine work")
    }
}

Coroutine에서 Error Handling은 생각보다 내용이 많아 추후에 따로 정리하기로 한다.
간단하게 이런게 있다 정도로 생각하고 넘어가면 될 것 같다.

Context 결합

CoroutineContext는 각 Context들을 결합할 수 있다.
CoroutineExceptionHandler 의 예시코드를 보면 Dispatchers.Default + exceptionHandler 와 같이 + 기호를 통해 Context를 결합했다.

이럴 수 있는 이유는 CoroutineContext에서 연산자 오버로딩을 통해 Context를 결합할 수 있도록 구현해 놓았다.

그러면 - 기호를 통해 뺄 수도 있을까 싶지만 그렇지 않다.
minusKey(key: Key) 라는 메서드를 통해 결합에서 제외시킬 수 있다.

아래는 + 구현 부분이다.

마무리

종합적으로 살펴보면 CoroutineContext는 실행될 스레드를 지정하고 예외를 처리하고 코루틴이 실행되는 환경을 조성하는 역할을 담당한다.

물론 CoroutineContext는 더 많은 내용이 있다.
스레드 관련해서 newSingleThreadContext, newFixedThreadPoolContext, asContextElement 등의 메소드들을 제공한다.

이런 부분은 추후에 필요할 때 추가하면 좋을 것 같다.

profile
느리더라도 한걸음씩

0개의 댓글