[Kotlin] Coroutine의 IO vs Default Dispatcher 비교

Doach Gosum·2023년 1월 23일
0

코틀린 이해하기

목록 보기
1/1

본 글은 2020.9.26 티스토리에서 작성한 글을 이전한 것입니다.

약간의 내용 첨삭이 있습니다.
원문링크 : https://sandn.tistory.com/110



Dispatcher
Dispatcher는 코루틴을 특정 스레드에서 실행할 수 있도록 도와주는 기능이다.
코루틴에서는 디스패처를 이용하여 다양하게 스코프를 지정할 수 있다.
Rx류의 라이브러리에서 쓰이는 스케쥴러가 유사한 기능을 한다.


출처- https://www.slideshare.net/BartomiejOsmaek/kotlin-coroutines-the-new-async


특히 비동기 백그라운드 작업을 수행할 때 가장 많이 쓰이는 것이 IO 와 Default Dispatcher이다.
Coroutine을 처음 접하면 이 두 디스패처 중 어떤 것을 써야 하는지 헷갈릴 수 있다.

정확히 이 두 디스패처의 차이가 무엇인지 알아보자.

Dispatchers.Default

다음은 Default Dispatcher에 대한 공식 설명이다.

  • The default [CoroutineDispatcher] that is used by all standard builders like
  • [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc
  • if no dispatcher nor any other [ContinuationInterceptor] is specified in their context.
  • It is backed by a shared pool of threads on JVM. By default, the maximal level of parallelism used
  • by this dispatcher is equal to the number of CPU cores, but is at least two.
  • Level of parallelism X guarantees that no more than X tasks can be executed in this dispatcher in parallel.

요약하자면, 해당 Dispatcher는 JVM의 공유 스레드풀을 사용하고 동시 작업 가능한 최대 갯수는 CPU의 코어 수와 같다고 한다. (최소 2개)

만약 4코어 CPU를 사용하는 경우 최대 4개의 병렬작업이 가능하다.

확인해보기 위해 약 50개의 Coroutine을 Default Dispatcher 위에서 실행시켜 보았다.

repeat(50) { 
	CoroutineScope(Dispatchers.Default).launch { 
		println(Thread.currentThread().name) 
	} 
}

코드의 결과는 다음과 같다.

DefaultDispatcher-worker-1 
DefaultDispatcher-worker-11 
DefaultDispatcher-worker-10 
DefaultDispatcher-worker-9 
DefaultDispatcher-worker-7 
... 
DefaultDispatcher-worker-7 
DefaultDispatcher-worker-9 
DefaultDispatcher-worker-7 
DefaultDispatcher-worker-8 
DefaultDispatcher-worker-11 
DefaultDispatcher-worker-5 
DefaultDispatcher-worker-3 
DefaultDispatcher-worker-4 
DefaultDispatcher-worker-10 
DefaultDispatcher-worker-12 
DefaultDispatcher-worker-6 
DefaultDispatcher-worker-6 
DefaultDispatcher-worker-1 
DefaultDispatcher-worker-2 
DefaultDispatcher-worker-1 
DefaultDispatcher-worker-6 
DefaultDispatcher-worker-1 
DefaultDispatcher-worker-12 
... Process finished with exit code 0

현재 사용하고 있는 CPU의 경우 6코어 12스레드이므로 약 12개의 스레드풀이 생성된다.
출력값을 보면 1~12번 스레드 위에서 작업이 실행되고 있는 것을 볼 수 있다.
따라서 작업들은 한번에 최대 12개씩만 실행된다.


Dispatchers.IO

그렇다면 IO Dispatcher는 어떨까?

다음은 IO Dispatcher에 대한 설명이다.

  • The [CoroutineDispatcher] that is designed for offloading blocking IO tasks to a shared pool of threads.
  • Additional threads in this pool are created and are shutdown on demand.
  • The number of threads used by this dispatcher is limited by the value of
  • "kotlinx.coroutines.io.parallelism" ([IO_PARALLELISM_PROPERTY_NAME]) system property.
  • It defaults to the limit of 64 threads or the number of cores (whichever is larger).
  • This dispatcher shares threads with a [Default][Dispatchers.Default] dispatcher, so using
  • withContext(Dispatchers.IO) { ... } does not lead to an actual switching to another thread —
  • typically execution continues in the same thread.

설명에 의하면 IO Dispathcher는 필요에 따라 추가적으로 스레드를 더 생성하거나 줄일 수 있으며 최대 64개까지 생성이 가능하다.
또한 Default Dispatcher와 스레드를 공유하기 때문에 switching으로 인한 오버헤드를 일으키지 않는다고 되어 있다.

코드를 실행해보면 다음과 같은 결과가 나온다.

... 
DefaultDispatcher-worker-61 
DefaultDispatcher-worker-62 
DefaultDispatcher-worker-3 
DefaultDispatcher-worker-11 
DefaultDispatcher-worker-29 
DefaultDispatcher-worker-64 
DefaultDispatcher-worker-21
DefaultDispatcher-worker-67 
...

Default Dispatcher와는 다르게 12개 이상의 스레드를 사용하는 것을 볼 수 있다.

다만, 분명 공식문서에서는 64개의 스레드를 풀에 생성하여 사용한다고 되어 있는데 어찌된 영문인지 스레드 번호는 64개 이상이 로그에 찍히는 것이 의아하였다.

구글링을 해보아도 아무런 단서가 나오지 않는 상황...

이해가 가지 않아 이에 대해 코틀린 공식 문서에 이슈를 남겨두었더니 공식문서가 수정되었다.

기존에는 아래 문구를 참고하여 스레드가 64개일 거라고 생각했었는데

It defaults to the limit of 64 threads or the number of cores (whichever is larger).

문서상 애매한 부분이 있었는지 아래의 설명을 문서에 추가해주었다.

  • As a result of thread sharing, more than 64 (default parallelism) threads can be created (but not used)
  • during operations over IO dispatcher.

즉, 스레드 번호는 초과하더라도 최대 실행 스레드는 64개로 일정하다.

공식 깃허브에 남겼던 Issue 링크를 남겨둔다.
https://github.com/Kotlin/kotlinx.coroutines/issues/2272


실험해보기

확인해보기 위해 대기시간이 있는 네트워크 작업을 통해 두 디스패처의 속도를 한번 비교해보자.
수십 개 정도의 간단한 크롤링 요청 작업이다.

// IO Dispatcher 
2020-09-26 13:38:07.404 21550-21638/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 3620 ms 
2020-09-26 13:38:17.391 21550-21720/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 3433 ms 
2020-09-26 14:58:48.877 22750-22855/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 4024 ms 
2020-09-26 14:59:15.670 22927-23000/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 4071 ms 
2020-09-26 14:59:34.632 23069-23126/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 3694 ms
// Default Dispatcher 
2020-09-26 14:58:05.095 22506-22552/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 4764 ms 
2020-09-26 14:58:26.333 22620-22664/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 4489 ms 
2020-09-26 14:59:59.606 23225-23292/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 4642 ms 
2020-09-26 15:00:18.112 23353-23396/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 5155 ms 
2020-09-26 15:00:34.161 23467-23511/com.ondarm.android.newsreaders D/MyTime: 걸린시간: 5019 ms

동일한 크롤링 작업을 5번 정도 반복한 결과값이다. IO Dispatcher 의 경우 Default Dispatcher 에 비해 평균 1~2초 정도 빠른 처리 속도를 보여준다.

이는 네트워크를 통한 데이터 호출의 경우 단순히 응답을 대기하고 있는 비중이 크기 때문에
스레드의 갯수가 늘어나는 만큼 자원을 더 효율적으로 사용할 수 있기 때문이다.

결론

그렇다면 각각 어떤 목적으로 사용하는 것이 좋을까?

앞서 보았듯 IO Dispatcher 의 경우 네트워크 입출력과 같은 대기시간이 긴 작업에 적합하다.
실질적인 연산보다는 입출력을 위해 자원을 사용하지 않고 대기하는 시간이 길기 때문에 코어 수 보다 많은 스레드를 생성하여 작업을 처리하는 것이 효율적인 것이다.

반대로, 방대한 계산처럼 CPU 집약적인 연산을 수행하는 경우라면 멀티코어의 효율을 최대한 끌어낼 수 있도록 코어 수 만큼의 스레드만 사용하는 Default Dispatcher 를 사용하는 것이 적합할 것이다.

profile
기본 그리고 간결함

0개의 댓글