관리 메뉴

나만을 위한 블로그

[코틀린 코루틴] 12. 디스패처 본문

책/코틀린 코루틴

[코틀린 코루틴] 12. 디스패처

참깨빵위에참깨빵 2024. 3. 17. 00:03
728x90
반응형

코루틴의 중요 기능은 코루틴이 실행돼야(시작, 재개 등) 할 쓰레드(또는 쓰레드 풀)를 결정할 수 있는 것이다. 디스패처로 이 기능을 쓸 수 있다.

코루틴이 어떤 쓰레드에서 실행될지 정하는 건 CoroutineContext다.

 

기본 디스패처

 

디스패처를 설정하지 않으면 기본 설정되는 디스패처는 CPU 집약적 연산을 수행하게 설계된 Dispatchers.Default다.

이 디스패처는 코드가 실행되는 컴퓨터의 CPU 개수와 같은 수(최소 2개 이상)의 쓰레드 풀을 갖고 있다. 쓰레드를 효율적으로 쓴다고 가정하면 이론적으로 최적의 쓰레드 개수라고 할 수 있다.

 

기본 디스패처 제한하기

 

고비용 작업이 Dispatchers.Default의 쓰레드를 다 써서 같은 디스패처를 쓰는 다른 코루틴이 실행될 기회를 제한하고 있다고 의심되는 상황을 가정한다. 이 때 코루틴 1.6에 도입된 Dispatchers.Default의 limitedParallelism을 쓰면 디스패처가 같은 쓰레드 풀을 쓰지만 같은 시간에 특정 수 이상의 쓰레드를 못 쓰게 제한할 수 있다.

디스패처의 쓰레드 수를 제한하는 방법은 Dispatchers.Default에만 쓰이는 게 아니라서 limitedParallelism을 기억해야 한다. Dispatchers.IO에서 limitedParallelism이 중요하고 자주 쓰이기 때문이다.

 

메인 디스패처

 

안드로이드를 포함한 앱 프레임워크는 가장 중요한 메인 쓰레드 개념을 갖고 있다.

안드로이드에서 메인 쓰레드는 UI와 상호작용하는 데 쓰는 유일한 쓰레드다. 메인 쓰레드는 자주 쓰여야 하지만 블로킹되면 전체 앱이 멈춰서 조심히 다뤄야 한다. 메인 쓰레드에서 코루틴을 실행하려면 Dispatchers.Main을 쓰면 된다.

블로킹 대신 중단하는 라이브러리를 쓰고 복잡한 연산을 안 한다면 이것만으로 충분하다. CPU 집약적 작업을 해야 한다면 Dispatchers.Default로 실행한다.

 

IO 디스패처

 

이것은 파일을 읽고 쓰는 경우, 안드로이드의 쉐어드 프리퍼런스를 쓰는 경우, 블로킹 함수 호출처럼 I/O 연산으로 쓰레드를 블로킹할 때 쓰기 위해 설계됐다. 아래 코드는 Dispatches.IO가 같은 시간에 50개 이상의 쓰레드를 쓸 수 있게 만들어져서 1초만 걸린다.

 

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlin.system.measureTimeMillis

suspend fun main(): Unit = coroutineScope {
    val time = measureTimeMillis {
        coroutineScope {
            repeat(50) {
                launch(Dispatchers.IO) {
                    Thread.sleep(1000)
                }
            }
        }
    }
    println(time) // 1107
}

 

Dispatchers.Default와 IO는 같은 쓰레드 풀을 공유한다. 두 디스패처 모두 최대로 쓰는 경우를 가정한다면, 이 경우 활성화된 쓰레드 개수는 쓰레드 한도 전부를 합친 것과 같다.

IO 디스패처를 쓰는 흔한 경우는 라이브러리에서 블로킹 함수를 호출해야 하는 경우다. 이 때 withContext(Dispatchers.IO)로 래핑해서 중단 함수로 만드는 게 가장 좋다.

 

class DiscUserRepository(
    private val discReader: DiscReader
): UserRepository {
    override suspend fun getUser(): UserData =
        withContext(Dispatchers.IO) {
            UserData(discReader.read("userName"))
        }
}

 

정해진 수의 쓰레드 풀을 가진 디스패처

 

몇몇 개발자들은 자신들이 쓰는 쓰레드 풀을 직접 관리하길 원하고 자바는 이를 위한 API를 제공한다.

Executors 클래스를 써서 쓰레드 수가 정해진 쓰레드 풀이나 캐싱된 쓰레드 풀을 만들 수 있다. 이렇게 만들어진 쓰레드 풀은 ExecutorService, Executor 인터페이스를 구현하며 asCoroutineDispatcher 함수를 써서 디스패처로 바꿀 수도 있다.

 

val NUMBER_OF_THREADS = 20
val dispatcher = Executors.newFixedThreadPool(NUMBER_OF_THREADS)
    .asCoroutineDispatcher()

 

 

제한받지 않는 디스패처

 

Dispatchers.Unconfined는 쓰레드를 바꾸지 않는단 점에서 이전 디스패처들과 다르다. 제한받지 않는 디스패처가 시작되면 시작한 쓰레드에서 실행된다. 재개됐을 때는 재개한 쓰레드에서 실행된다.

이 디스패처는 단위 테스트 시 유용하다. launch를 호출하는 함수를 테스트해야 할 경우 시간을 동기화하는 건 쉽지 않다.

이 경우 Dispatchers.Unconfined로 다른 디스패처를 대체해서 쓸 수 있다. 모든 스코프에서 제한받지 않는 디스패처를 쓰면 모든 작업이 같은 쓰레드에서 실행되어 연산 순서를 쉽게 통제할 수 있다. 하지만 runTest를 쓰면 이 방법은 필요없다.

성능 면에서 보면 쓰레드 스위칭을 일으키지 않아서 제한받지 않는 디스패처의 비용이 가장 저렴하다. 실행되는 쓰레드를 전혀 신경쓰지 않아도 된다면 제한받지 않는 디스패처를 선택해도 된다.

하지만 현업에서 이 디스패처를 쓰는 건 무모하다. 블로킹 호출을 하는데도 실수로 메인 쓰레드에서 실행한다면 전체 앱이 블로킹되는 참사가 발생한다.

반응형
Comments