Skip to content

디스패처

Published: at 오후 03:05

Table of contents

Open Table of contents

디스패처

코틀린 코루틴 라이브러리가 제공하는 중요한 기능을 코루틴이 실행되어야 할 스레드를 결정할 수 있다는 것입니다. 디스패처를 이용해 이러한 기능을 사용할 수 있습니다.

코틀린 코루틴에서 코루틴이 어떤 스레드에서 실핼될지 정하는 것은 CoroutineContext입니다.

기본 디스패처

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

이 디스패처는 코드가 실행되는 컴퓨터의 CPU 개수와 동일한 수의 스레드 풀을 가지고 있습니다.

스레드를 효율적으로 사용하고 있다고 가정하면 이론적으로는 최적의 스레드 수라고 할 수 있습니다.

suspend fun main() = coroutineScope {
  repeat(100_000) {
    launch {
      List(1000) { Random.nextLong() }.maxOrNull()
      val threadName = Thread.currentThread().name
      println("Running in thread $threadName")
    }
  }
}

기본 디스패처를 제한하기

비용이 많이 드는 작업이 Dispatchers.Default의 스레드를 다 써버려서 같은 디스패처를 사용하는 다른 코루틴이 실행될 기회를 제한하고 있다고 의심하는 상황을 떠올려 봅시다.

이런 상황을 마주쳤을 때 Dispatchers.DefaultlimitParallelism을 사용하 디스패처가 같은 스레드 풀을 사용하지만 같은 시간에 특정 수 이상의 스레드를 사용하지 못하도록 제한할 수 있습니다.

디스패처의 스레드 수를 제한하는 방법은 Dispatchers.Default에만 사용되는 것이 아니기 때문에 limitParallelism을 기억하고 있어야 합니다.

메인 디스패처

일반적으로 안드로이드를 포함한 애플리케이션 프레임워크는 가장 중요한 스레드인 메인 또는 UI 스레드 개념을 가지고 있습니다.

메인 스레드는 UI와 상호작용하는 데 사용하는 유일한 스레드입니다. 메인 스레드는 자주 사용되어야 하지만 아주 조심스럽게 다뤄야 합니다.

메인스레드가 블로킹되면 전체 애플리케이션이 멈춰 버립니다.

메인 스레드에서 코루틴을 실행 하려면 Dispatchers.Main을 사용하면 됩니다.

IO 디스패처

Dispatchers.IO는 파일을 읽고 쓰는 경우, 알드로이드의 셰어드 프레퍼런스를 사용하는 경우, 블로킹 함수를 호출하는 경우처럼 I/O 연산으로 스레드를 블로킹할 때 사용하기 위해 설계되었습니다.

다음 코드는 Dispatchers.IO가 같은 시간에 50개가 넘는 스레드를 사용할 수 있도록 만들어졌기 때문에 1초밖에 걸리지 않습니다.

suspend fun main() {
    val time = measureTimeMillis {
        coroutineScope {
            repeat(100_000) {
                launch(Dispatchers.IO) {
                    Thread.sleep(1000)
                }
            }
        }
    }
    println("Took $time ms") // Took 1000 ms
}

스레드가 무한한 풀이 있다고 가정했을 때, 처음에는 풀이 비어있지만 더 많은 스레드가 필요해지만 스레드가 생성되고 작업이 끝날 때까지 활성화된 상태로 유지됩니다.

이러한 스레드 풀이 존재하더라도 직접 사용하는 건 안전하다고 볼 수 없습니다.

활성화된 스레드가 너무 많다면 성능이 점ㅈ머 떨어지게 되고 결국에는 메모리 부족 에러가 일어날 것입니다.

따라서 같은 시간에 사용할 수 있는 스레드 수를 제한한 디스패처가 필요합니다.

Dispatchers.Default는 는 프로세서가 가지고 있는 코어 수로 제한이 됩니다.

Dispatchers.IO는 64개(또는 더 많은 코어가 있다면 해당 코어의 수)로 제한이 됩니다.

suspend fun main() = coroutineScope {
    repeat(1000) {
        launch(Dispatchers.IO) {
            Thread.sleep(200)

            val threadName = Thread.currentThread().name
            println("Running in thread $threadName")
        }
    }
}

좀더 자세히 살펴보기 위해 Dispatchers.DefaultDispatchers.IO 둘 모두를 최대치로 사용하는 경우를 생각해 봅시다.

이렇게 할 경우 활성화된 스레드의 수는 한도 전부를 합친것과 같습니다.

Dispatchers.IO에서 64개의 스레드까지 사용할 수 있고, 8개의 코어를 가지고 있다면 공유 스레드 풀에서 활성화된 스레드는 72개일 것입니다.

스레드 재활용적인 측면에서 효율적이라 할 수 있으며, 디스패처의 스레드 수는 각각 별개로 설정됩니다.

Dispatchers.IO를 사용하는 가장 흔한 경우는 라이브러리에서 블로킹 함수를 호출해야 하는 경우입니다.

이런 경우 withContext(Dispatchers.IO)로 래핑해 중단 함수로 만드는 것이 가장 좋습니다.

커스텀 스레드 풀을 사용하는 IO 디스패처

Dispatchers.IO에는 limitParallelism 함수를 위해 정의된 특별한 작동 방식이 있습니다.

limitParallelism 함수는 독립적인 스레드 풀을 가진 새로운 디스패처를 만듭니다.

이렇게 만들어진 풀은 우리가 원하는 만큼 많은 수의 스레드 수를 설정할 수 있으므로 스레드 수가 64개로 제한되지 않습니다.

100개의 코루틴이 각각 스레들르 1초씩 블로킹하는 경우를 생각해 봅시다.

이러한 코루틴을 Dispatchers.IO에서 실행하면 2초가 걸립니다. 동일한 동작을 limitParallelism으로 100개의 스레드를 사용하는 Dispatchers.IO에서 실행하면 1초가 걸립니다.

디스패처의 한도는 서로 무관하기 때문에 디스패처의 실행 시간을 동시에 측정할 수 있습니다.

suspend fun main(): Unit = coroutineScope {
    launch {
        printCoroutineTime(Dispatchers.IO)
        // Dispatchers.IO took: 2074
    }

    launch {
        val dispatcher = Dispatchers.IO.limitParallelism(100)
        printCoroutineTime(dispatcher)
        // LimitedDispatcher@XXS took: 1082
    }
}

suspend fun printCoroutineTime(dispatcher: CoroutineDispatcher) {
    val time = measureTimeMillis {
        coroutineScope {
            repeat(100) {
                launch(dispatcher) {
                    Thread.sleep(1000)
                }
            }
        }
    }
    println("$dispatcher took: $time")
}

limitParallelism을 가장 잘 활용하는 방법은 스레드를 블로킹하는 경우가 잦은 클래스에서 자기만의 한도를 가진 커스텀 디스패처를 정의하는 것입니다.

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

몇몇 개발자들은 자신들이 사용하는 스레드 풀을 직접 관리하기를 원하며, 자바는 이를 지원하기위해 강력한 API를 제공합니다.

예를 들면 Executors 클래스를 스레드 수가 정해져 있는 스레드 풀이나 캐싱된 스레드 풀을 만들 수 있습니다.

싱글스레드로 제한된 디스패처

다수의 스레드를 사용하는 모든 디스패처에서는 공유 상태로 인한 문제를 생각해야 합니다.

이런 문제를 해결하는 다양한 방법이 있으며, 싱글 스레드를 가진 디스패처를 사용하는 방법이 그중 하나입니다.

싱글 스레드를 사용하면 동기화를 위한 조치가 더 이상 필요하지 않습니다.

Executors를 사용하여 싱글스레드 디스패처를 만드는 방법이 대표적입니다.

val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

val dispatcher = newSingleThreadContext("MyOwnThread")

하지만 디스패처가 스레드 하나를 액티브한 상태로 유지하고 있으며, 더 이상 사용되지 않을 때는 스레드를 반드시 닫아야 한다는 문제점이 있습니다.

최근에는 Dispatcher.Default나 (스레드를 블로킹한다면) 병렬 처리를 1로 제한한 Dispatcher.IO를 주로 사용합니다.

단 하나의 스레드만 가지고 있기 때문에 이 스레드가 블로킹되면 작업이 순차적으로 처리되는 것이 가장 큰 단점입니다.

프로젝트 룸의 가상 스레드 사용하기

JVM 플랫폼은 프로젝트 룸이라는 새로운 기술을 발표했습니다.

프로젝트 룸의 가장 혁신적인 특징은 일반적인 스레드보다 훨씬 가벼운 가상 스레드를 도입했다는 점입니다.

일반적인 스레드를 블로킹하는 것보다 가상 스레드를 블로킹하는 것이 비용이 훨씬 적게 듭니다.

코틀린 코루틴을 알고 있는 개발자들을 프로젝트 룸을 사용할 필요가 별로 없습니다.

코틀린 코루틴은 취소를 쉽게 할 수 있으며, 테스트에서 가상 시간을 쓰는 등의 훨씬 더 놀라운 기능을 갖추고 있습니다.

제한받지 않는 디스패처

마지막으로 생각해 봐야 할 디스패처는 Dispatchers.Unconfined입니다. 이 디스패처는 스레드를 바꾸지 않는다는 점에서 이전 디스패처들과 다릅니다.

제한받지 않은 디스패처가 시작되면 시작한 스레드에서 실행이 됩니다.

재개되었을 떄는 재개한 스레드에서 실행이 됩니다.

suspend fun main() {
    withContext(newSingleThreadContext("Thread1") {
        var continuation: Continuation<Unit>? = null

        lanch(newSingleThreadContext("Thread2") {
            delay(1000)
            continuation?.resume(Unit)
        }

        launch(Dispatchers.Unconfined) {
            println(Thread.currentThread().name) // Thread1

            suspendCancelableCoroutine<Unit> {
                continuation = it
            }

            println(Thread.currentThread().name) // Thread2

            delay(1000)

            println(Thread.currentThread().name)
            // kotlinx.coroutines.DefaultExecutor
        }
    }
}

제한받지 않는 디스패처는 단위 테스트할 때 유용합니다. launch를 호출하는 함수를 테스트해야 된다고 생각해 봅시다.

시간을 동기화하는 건 쉽지 않습니다.

이런 경우 Dispatchers.Unconfined로 다른 디스패처를 대체하여 사용할 수 있습니다.

모든 스코프에서 제한받지 않는 디스패처를 사용하면 모든 작업이 같은 스레드에서 실행되기 때문에 연산의 순서를 쉽게 통제할 수 있습니다.

하지만 kotlinx-coroutines-testrunTest를 사용하면 이런 방법은 필요하지 않습니다.

성능적인 측면에서 보면 스레드 스위칭을 일으키지 않는다는 점에서 제한받지 않는 디스패처의 비용이 가장 저렴합니다.

하지만 현업에서 제한받지 않는 디스패처를 사용하는 건 무모하다고 볼 수 있습니다. 블로킹 호출을 하는데도 실수로 Main스레드에서 실행한다면 전체 애플리케이션이 블로킹되는 참사가 발생하게 됩니다.

작업의 종류에 따른 각 디스패처의 성능 비교