3장에서 다루는 내용

  • 코루틴 디스패처 객체의 역할
  • 제한된 디스패처와 무제한 디스패처
  • 제한된 디스패처 생성
  • 코루틴 디스패처로 코루틴 실행하기
  • 코루틴 라이브러리에 미리 정의된 디스패처의 종류와 사용처

3-1. CoroutineDispatcher란 무엇인가?

Dispatcher

  • Dispatch → 보내다
  • Dispatcher → 무언가를 보내는 주체

따라서 CoroutineDispatcher → 코루틴을 보내는 주체

CoroutineDispatcher는 코루틴을 스레드로 보냄

  • 코루틴은 일시중단 가능한 “작업”이기 때문에 스레드가 있어야 실행할 수 있음
  • 코루틴을 스레드로 보내는데 사용할 수 있는 스레드나 스레드 풀을 가짐
  • 코루틴을 실행 요청한 스레드에서 코루틴이 실행되도록 만들 수 있음

3-1-1. CoroutineDispatcher의 동작 살펴보기

  • 2개의 스레드로 구성된 스레드풀을 갖는 코루틴 디스패처 객체
  • 실행돼야 하는 작업을 저장하는 작업 대기열을 가짐
  • 새로운 코루틴 실행 요청이 들어오면 해당 작업을 작업 대기열에 적재
  • 코루틴을 실행할 수 있는 스레드가 있으면 코루틴을 해당 스레드로 보내 실행시킴
  • 사용 가능한 스레드가 없다면 사용 가능한 스레드가 생길 때까지 스레드에 작업을 보내지 않고 작업 대기열에서 대기
  • 사용 가능한 스레드가 생기면 대기열에서 대기중인 코루틴을 스레드로 보냄

코루틴 디스패처 객체는 자신에게 실행 요청된 코루틴을 우선 작업 대기열에 적재한 후 사용할 수 있는 스레드가 생기면 스레드로 보내는 방식으로 동작

3-1-2. CoroutineDispatcher의 역할

코루틴 디스패처는 코루틴의 실행을 관리하는 주체

자신에게 실행 요청된 코루틴들을 작업 대기열에 적재하고 자신이 사용할 수 있는 스레드가 새로운 작업을 실행할 수 있는 상태라면 스레드로 코루틴을 보내 실행할 수 있게 만드는 역할

코루틴 디스패처는 코루틴 실행요청이 들어오면 일반적으로 작업 대기열에 적재한 후에 스레드로 보냄
하지만 실행 옵션에 따라 작업 대기열에 적재되지 않고 즉시 실행될 수도 있음
작업 대기열이 없는 코루틴 디스패처 구현체도 있으니 이는 매우 예외적인 경우임
이는 무제한 디스패처에서 별도로 다룸

3-2. 제한된 디스패처와 무제한 디스패처

코루틴 디스패처는 2가지 종류가 있음

  • 제한된 디스패처 → 사용할 수 있는 스레드나 스레드풀이 제한된 디스패처
  • 무제한 디스패처 → 사용할 수 있는 스레드나 스레드풀이 제한되지 않은 디스패처

일반적으로 코루틴 디스패처는 객체별로 어떤 작업을 처리할지 미리 역할을 부여하고 역할에 맞춰 실행을 요청하는 것이 효율적이기 때문에 대부분의 디스패처는 제한된 디스패처임

입출력 작업 → 입출력 작업용 콜틴 디스패처 객체에 실행을 요청
CPU 연산 작업 → CPU 연산 작업용 코루틴 디스패처 객체에 실행을 요청

무제한 디스패처라고 해서 코루틴이 아무 스레드에서나 실행되는 것은 아님

  • 실행 요청된 코루틴이 이전 코드가 실행되던 스레드에서 계속해서 실행되도록 함
  • 실행되는 스레드가 매번 달라질 수 있고 특정 스레드로 제한되어 있지 않기 때문에 무제한 디스패처라고 불림
  • 11-3에서 자세히 다룰 예정

3-3. 제한된 디스패처 생성하기

코루틴 라이브러리에서 제공하는 제한된 디스패처 생성 함수를 다룸

3-3-1. 단일 스레드 디스페처

단일 스레드 디스패처 → 사용할 수 있는 스레드가 하나

newSingleThreadContext() 함수를 통해 만들 수 있음

  • 이 함수는 문자열 타입의 name을 인자로 받음
  • name → 디스패처에서 관리하는 스레드 이름
  • 반환 타입은 CoroutineDispatcher임
val dispatcher : CoroutineDispatcher = newSingleThreadContext(name = "SingleThread")

이 함수를 통해 만들어진 디스패처는 작업 대기열과 스레드 하나로 구성된 스레드풀을 사용할 수 있음

이 스레드의 이름은 인자로 넘긴 “SingleThread”가 됨

3-3-2. 멀티 스레드 디스패처 만들기

코루틴 라이브러리의 newFixedThreadPoolContext() 함수를 통해 만들 수 있음

  • 스레드의 개수 (nThreads)와 스레드의 이름 (name)을 매개변수로 받음
  • 만들어지는 스레드들은 인자로 받은 name값 뒤에 ‘-1’부터 시작해 숫자가 하나씩 증가하는 형식으로 이름을 붙임
    • Thread-1, Thread-2…
val multiThreadDiapatcher: CoroutineDispatcher = newFixedThreadPoolContext(
	nThreads = 2,
	name = "MultiThread"
)

이 함수를 통해 만들어진 디스패처는 작업 대기열과 MultiThread-1, MultiThread-2라는 이름을 가진 스레드 2개가 존재함

이렇게 만들어진 디스패처는 newSingleThreadContext()로 만들어진 디스패처와 모습이 매우 흡사함

newSingleThreadContext()가 내부적으로 newFixedThreadPoolContext()를 사용하도록 구현되어 있기 때문임

public fun newSingleThreadContext(name: String): 
ClosableCoroutineDispatcher = 
	newFixedThreadPoolContext(1, name)

3-4. CoroutineDispatcher 사용해 코루틴 실행하기

3-4-1. launch의 파라미터로 CoroutineDispatcher 사용하기

  1. 단일 스레드 디스패처 사용해 코루틴 실행하기

    launch함수를 사용해 만든 코루틴을 특정 코루틴 디스패처 객체에 실행 요청하기 위해서는 launch 함수의 context 인자로 코루틴 디스패처 객체를 넘기면 됨

    fun main() = runBlocking<Unit> {
    	val dispathcer = newSingleThreadContext(name = "SingleThread")
    	launch(context = dispatcher) {
    		println("[${Thread.currentThread().name}] 실행")
    	}
    }
    
    // [SingleThread @coroutine#2] 실행

    새로운 스레드의 이름을 SingleThread로 설정했으므로 launch 함수를 통해 생성된 코루틴이 실행되는 스레드가 SingleThread로 나옴

  2. 멀티 스레드 디스패처를 사용해 코루틴 실행하기

    단일 스레드 디스패처를 사용하는 방식과 동일

    launch 함수의 context 인자로 multiThreadDispatcher를 넘기면 됨

    fun main() = runBlocking<Unit> {
    	val dispathcer = newFixedThreadPoolContext(
    		nThreads = 2,
    		name = "SingleThread"
    	)
    	launch(context = dispatcher) {
    		println("[${Thread.currentThread().name}] 실행")
    	}
    	launch(context = dispatcher) {
    		println("[${Thread.currentThread().name}] 실행")
    	}
    }

    단일 스레드 코루틴 디스패처와 마찬가지로 실행을 요청받은 코루틴을 작업 대기열에 적재하고 작업 가능한 스레드에 코루틴을 보내 실행

    다음 작업도 마찬가지로 작업 대기열에 적재 한 후 사용 가능한 스레드에 코루틴을 보내 실행함

    이 때 실행 환경에 따라 각 코루틴이 다른 속도로 처리될 수 있기 때문에 출력되는 스레드 이름이 같을 수 있음

3-4-2. 부모 코루틴의 CoroutineDispatcher를 사용해 자식 코루틴 실행하기

코루틴은 구조화를 제공해 콜틴 내부에서 새로운 코루틴을 실행할 수 있음

이 때 바깥쪽 코루틴을 부모 코루틴, 내부에서 실행되는 코루틴을 자식 코루틴이라고 함

구조화는 코루틴을 계층 관계로 만드는 것 뿐 아니라 부모 코루틴의 실행 환경을 자식 코루틴에 전달하는 데도 사용됨

만약 자식 코루틴에 코루틴 디스패처 객체가 설정되지 않았으면 부모 코루틴의 코루틴 디스패처 객체를 사용함

fun main() = runBlocking<Unit> {
	val dispathcer = newFixedThreadPoolContext(
		nThreads = 2,
		name = "SingleThread"
	)
	launch(context = dispatcher) { // 부모
		println("[${Thread.currentThread().name}] 부모 코루틴 실행")
		
		launch(context = dispatcher) { // 자식
			println("[${Thread.currentThread().name}] 자식 코루틴 실행")
		}
		
		launch(context = dispatcher) { // 자식
			println("[${Thread.currentThread().name}] 자식 코루틴 실행")
		}
	}
}

/*
[MultiThread-1 @coroutine#2] 부모 코루틴 실행
[MultiThread-2 @coroutine#3] 자식 코루틴 실행
[MultiThread-1 @coroutine#4] 자식 코루틴 실행
*/

가장 상단의 launch 함수로 생성되는 코루틴이 부모 코루틴이 됨

이 부모 코루틴은 전용 스레드가 2개인 코루틴 디스패처를 사용

부모 코루틴 launch 함수의 람다식 내부에서 2개의 launch 함수를 호출해2개의 코루틴을 추가로 생성하는데 이 때 생성되는 코루틴이 자식 코루틴임

이 자식 코루틴들에는 별도의 코루틴 디스패처가 설정되어 있지 않기 때문에 부모의 코루틴 디스패처를 사용함

부모 코루틴과 자식 코루틴이 동일한 코루틴 디스패처 객체를 사용하므로 스레드를 공유하는 것을 볼 수 있음

특정 코루틴 디스패처에서 여러 작업을 실행해야 한다면 부모 코루틴에 코루틴 디스패처 객체를 설정하고 그 아래 자식 코루틴을 여러개 생성하면 됨

3-5. 미리 정의된 CoroutineDispatcher

3-5-1. Dispatchers.IO

멀티 스레드 프로그래밍에서 가장 많이 사용되는 작업 → I/O 작업

  • 네트워크 통신을 위한 HTTP 요청
  • DB 작업

I/O 작업 여러 개를 동시에 수행하므로 이런 요청을 동시에 수행하기 위해서는 많은 스레드가 필요함

코루틴 라이브러리에서는 입출력 작업을 위해 미리 정의된 Dispatchers.IO를 제공함

Dispatchers.IO가 최대로 사용할 수 있는 스레드의 수 → JVM에서 사용할 수 있는 프로세서 수와 64 중 큰 값으로 설정되어 있음

Dispatchers.IO는 싱글톤 인스턴스이므로 launch 함수의 인자로 곧바로 넘겨줄 수 있음

fun main() = runBlocking<Unit> {
	launch(Dispatchers.IO) {
		println("[${Thread.currentThread().name}] 코루틴 실행")
	}
}

// [DefaultDispathcer-worker-1 @coroutine#2] 코루틴 실행

위 코드의 결과를 보면 코루틴이 실행된 스레드의 이름 앞에 DefaultDispatcher-worker가 붙은 것을 알 수 있음

DefaultDispatcher-worker가 붙은 스레드는 코루틴 라이브러리에서 제공하는 공유 스레드풀에 속한 스레드임

Dispatchers.IO는 공유 스레드풀의 스레드를 사용할 수 있도록 구현됐음

공유 스레드풀에 대해서는 추후 더 자세하게 다룰 예정

3-5-2. Dispatchers.Default

CPU 바운드 작업 → CPU 연산이 필요한 작업

ex) 대용량 데이터 처리

Dispatchers.Default → CPU 바운드 작업이 필요할 때 사용하는 CoroutineDispatcher

Dispatchers.Default도 그 자체로 싱글턴 인스턴스이므로 간단하게 호출할 수 있음

fun main() = runBlocking<Unit> {
	launch(Dispatchers.Default) {
		println("[${Thread.currentThread().name}] 코루틴 실행")
	}
}

// [DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행

입출력 작업과 CPU 바운드 작업

두 작업의 중요한 차이 → 실행됐을 때 스레드를 지속적으로 사용하는지 여부

일반적으로 입출력 작업은 작업(네트워크 요청, DB 조회 요청 등)을 실행한 후 결과를 반환받을 때까지 스레드를 사용하지 않음

CPU 바운드 작업은 작업을 하는 동안 스레드를 지속적으로 사용

이 차이로 인해 CPU 바운드 작업은 스레드 기반 작업을 사용했을 때와 코루틴을 사용했을 때 효율성에 차이가 발생함

입출력 작업에 코루틴을 사용 → 작업 실행 후 스레드가 대기하는 동안 해당 스레드에서 다른 작업을 동시에 실행할 수 있어 효츌적

CPU 바운드 작업에 코루틴 사용 → 스레드가 지속적으로 사용되기 때문에 스레드 기반 작업을 사용해 실행했을 때와 처리 속도에 차이가 거의 없음

I/O 작업CPU 바운드 작업
스레드 기반 작업 사용 시느림비슷
코루틴 사용 시빠름

3-5-3. limitedParallelism을 사용해 Dispatchers.Default 스레드 사용 제한하기

Dispatchers.Default를 사용해 무겁고 오래 걸리는 작업을 처리하면 특정 작업을 위해 Dispatchers.Default의 모든 스레드가 사용될 수 있으며, 특정 연산을 진행하는 동안 Dispatchers.Defualt를 사용하는 다른 작업이 실행되지 못할 수 있음

이를 방지하기 위해 코루틴 라이브러리는 Dispatchers.Default의 일부 스레드만 사용해 특정 연산을 실행할 수 있도록 하는 limitedParallelism 함수를 지원함

fun main() = runBlocking<Unit> {
	launch(Dispatchers.Default.limitedParallelism(2)) {
		repeat(10) {
			launch {
				println("[${Thread.currentThread().name}] 코루틴 실행")
			}
		}
	}
}

/*
[DefaultDispatcher-worker-2 @coroutine#3] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#4] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#5] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#6] 코루틴 실행
.
.
.
[DefaultDispatcher-worker-2 @coroutine#11] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#12] 코루틴 실행
*/
  • Dispatchers.Default.limitedParallelism(2)를 통해 Dispatchers.Default의 여러 스레드 중 2개의 스레드만 사용해 10개의 코루틴을 실행

3-5-4. 공유 스레드풀을 사용하는 Dispatchers.IO와 Dispatchers.Default

Dispatchers.IO와 Dispatchers.Default를 사용한 코드에서 발견할 수 있는 공통점 → 코루틴을 실행한 스레드의 이름에 DefaultDispatcher-worker가 표시됨

Dispatchers.IO와 Dispatchers.Default는 코루틴 라이브러리의 공유 스레드풀을 사용한다는 것을 의미

코루틴 라이브러리는 스레드의 생성과 관리를 효율적으로 할 수 있도록 애플리케이션 레벨의 공유 스레드풀을 제공

이 공유 스레드풀에는 스레드를 무한으로 생성할 수 있음

코루틴 라이브러리는 공유 스레드풀에 스레드를 생성하고 사용할 수 있도록 하는 API를 제공

Dispatchers.IO와 Dispatchers.Default는 모두 위에서 언급한 API를 사용해 구현됐기 때문에 공유 스레드풀을 사용함

다만 공유 스레드풀에서 Dispatchers.IO와 Dispatchers.Default가 사용하는 스레드는 구분됨

newFixedThreadPoolContext 함수로 만들어지는 디스패처는 자신만 사용할 수 있는 전용 스레드풀을 생성하는 것과 다르게 Dispatchers.IO와 Dispatchers.Default는 공유 스레드풀의 스레드를 사용함

Dispatchers.IO의 limitedParallelism

Dispatchers.Default에 limitedParallelism 함수를 사용하면 스레드풀의 스레드 중 일부만 사용하도록 제한할 수 있음

하지만 Dispatchers.IO의 limitedParallelism은 조금 다르게 동작함

Dispatchers.IO의 limitedParallelism은 공유 스레드풀의 스레드로 구성된 새로운 스레드풀을 만들어내며, 스레드의 수를 제한 없이 만들 수 있음

fun main() = runBlocking<Unit> {
	launch(Dispatchers.IO.limitedParallelism(100)) {
		reapeat(200) {
				launch {
					Thread.sleep(1000L)
					println("[${Thread.currentThread().name}] 코루틴 실행")
				}
		}
	}
}

/*
[DefaultDispatcher-worker-60] 코투린 실행
[DefaultDispatcher-worker-74] 코투린 실행
[DefaultDispatcher-worker-68] 코투린 실행
.
.
.
[DefaultDispatcher-worker-85] 코투린 실행
[DefaultDispatcher-worker-49] 코투린 실행
[DefaultDispatcher-worker-98] 코투린 실행
*/
  • Dispatchers.IO.limitedParallelism(100)를 통해 공유 스레드풀 상에서 100개의 스레드로 구성된 스레드풀을 만들어냄

Dispatchers.IO의 limitedParallelism함수는 공유 스레드풀 상에서 Dispatchers.IO나 Dispatchers.Default와 관계 없는 스레드로 구성된 스레드풀을 만들어냄

특정 작업이 다른 작업에 영향을 받지 않아야 해, 별도 스레드 풀에서 실행되는 것이 필요할 때 사용

다만 공유 스레드풀에서 새로운 스레드를 만들어내는 작업이라 비용이 비싼 작업이므로 남용하면 안됨

3-5-5. Dispatchers.Main

UI가 있는 애플리케이션에서 메인 스레드를 사용하기 위해 사용되는 특별한 CoroutineDispatcher 객체

코루틴 라이브러리 추가만으론 사용할 수 없고 별도의 라이브러리(ex. Android)를 추가해야 사용할 수 있음

UI 스레드(Main Thread)를 사용하는 라이브러리가 없는 상태로 Diapatchers.Main을 호출하면 IllegalStateException이 발생함

Dispatchers.Main의 구현체를 제공하는 라이브러리가 없어 생기는 오류임

3-6. 3장 요약

  • CoroutineDispatcher 객체는 코루틴을 스레드로 보내 실행하는 객체로 코루틴을 작업 대기열에 적재한 후 사용이 가능한 스레드로 보내 실행
  • 제한된 디스패처 → 사용할 수 있는 스레드가 특정 스레드 또는 스레드풀로 제한됨
  • 무제한 디스패처 → 코루틴을 실행하는 데 사용할 수 있는 스레드가 제한되지 않음
  • newSingleThreadContext와 newFixedThreadPoolContext 함수를 사용해 제한된 디스패처 객체를 생성할 수 있음
  • launch 함수로 코루틴을 실행할 때 context 인자로 CoroutineDispatcher 객체를 넘기면 해단 CoroutineDispatcher 객첼르 사용해 코루틴이 실행됨
  • 자식 코루틴은 기본적으로 부모 코루틴의 CoroutineDispatcher 객체를 상속받아 사용함
  • 코루틴 라이브러리는 미리 정의된 Dispatchers.IO, Dispatchers.Default, Dispatchers.Main을 제공함
  • Dispatchers.IO는 입출력 작업을 위한 디스패처 객체로, 네트워크 요청이나 파일 I/O등에 사용됨
  • Dispatchers.Default는 CPU 바운드 작업을 위한 디스패처 객체로, 대용량 데이터 처리 등에 사용됨
  • limitedParallelism() 함수를 사용해 특정 여산을 위해 사용되는 Dispatchers.Default의 스레드 수를 제한할 수 있음
  • Dispatchers.Default와 Dispatchers.IO는 코루틴 라이브러리에서 제공하는 공유 스레드풀을 사용함
  • Dispatchers.Main은 메인 스레드에서 실행되어야 하는 작업에 사용되는 디스패처로, 사용하기 위해서는 Dispatchers.Main의 구현체를 제공하는 라이브러리 의존성을 추가해야 함
  • Diapatchers.Main은 일반적으로 UI가 있는 애플리케이션에서 UI를 업데이트하는 데 사용됨

0개의 댓글