작성
·
19
·
수정됨
0
안녕하세요 강사님! 강의 잘 듣고 있습니다ㅎㅎ
강의상에서 코루틴을 다른 스레드에서 수행시키기 위한 방법으로 Dispatchers 클래스에 정의된 Default
이름의 CoroutineDispatcher를 코루틴 빌더의 인자로 넣어주셨는데요.
CoroutineDispatcher 구현체인 Default
, IO
, Unconfined
는 각각 어떤 차이점이 있는지가 궁금합니다.
내부 코드를 봤을땐 스레드 관리 방식과 스케줄링 방식 등에 차이가 있는 것 같은데, 각각의 Dispatcher가 언제 사용되는지, 가장 주요한 차이점은 뭔지가 궁금해요!!
그리고 각 Dispatcher는 내부적으로 스레드풀을 가지고 있고, 작업이 넘어왔을 때 내부적으로 관리하는 스레드 풀에서 스레드에 코루틴을 할당하는 방식으로 동작하는걸까요?
맞다면, Default 나 IO Dispatcher의 스레드 개수나 graceful shutdown 설정 등을 커스텀할 수 있는지도 궁금합니다!
답변 1
0
안녕하세요 기우님! ☺ 아주 좋은 질문 감사드립니다.
답변을 드리기에 앞서 https://myungpyo.medium.com/%EC%BD%94%EB%A3%A8%ED%8B%B4-%EB%94%94%EC%8A%A4%ED%8C%A8%EC%B3%90-%EC%A1%B0%EA%B8%88-%EB%8D%94-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0-92db58efca24 와 같은 좋은 글도 한 번 보고 오시면 좋을 것 같아요!
이제 하나씩 답변 드려 보겠습니다.
각 Dispatcher는 내부적으로 스레드풀을 가지고 있고, 작업이 넘어왔을 때 내부적으로 관리하는 스레드 풀에서 스레드에 코루틴을 할당하는 방식으로 동작하는걸까요?
이 내용 먼저 말씀드려볼게요!
네 맞습니다. Default
와 IO는
내부적으로 스레드 풀을 갖고 있고 (예를 들어 IO의 경우 최대 64개를 갖고 있습니다.) 코루틴이 들어오면 내부적으로 관리하는 pool 내의 thread에서 코루틴이 실행되고 중단되었다가 다시 pool 내의 어떤 thread에 배정해 코루틴을 실행하는 식으로 동작합니다.
다만, Default
의 스레드 풀을 IO
가 포함하고 있는 관계라 (ex. https://sandn.tistory.com/112) IO 에서 실행하더라도 Default 라는 thread name을 확인할 수도 있습니다.
Uncofined
는 조금 특이합니다. 자체 스레드 풀이 존재하지 않고, suspend 함수가 재개될 때의 그 스레드에서 다음 코드를 이어서 실행시켜 버려요! (Default
나 IO는
suspend 함수가 재개 되면 다시 내 pool의 스레드로 코드를 가져와 실행하죠) 이런 특수성 때문에 Unconfined
의 경우 실무에서 사용 할 일이 없는 Dispatcher 라고 봐주시면 될 것 같습니다.
내부 코드를 봤을땐 스레드 관리 방식과 스케줄링 방식 등에 차이가 있는 것 같은데, 각각의 Dispatcher가 언제 사용되는지, 가장 주요한 차이점은 뭔지가 궁금해요!!
그럼 남은 Dispatcher는 Default
와 IO
인데요! 간단하게 Default
는 CPU-intensive한 작업, I/O는 IO
-intensive한 작업에 사용하시면 됩니다. ☺
마지막으로
Default 나 IO Dispatcher의 스레드 개수나 graceful shutdown 설정 등을 커스텀할 수 있는지도 궁금합니다!
에 대해서도 답변 드리면 Dispatcher 자체적으로 graceful shutdown hook을 제공하지는 않지만, 마치 Executors 에 대해 저희가 직접 graceful shutdown 기능을 구현할 수 있는 것처럼 직접 구현할 수 있습니다.
(ex. https://easywritten.com/post/best-way-to-shutdown-executor-service-in-java/ 에서 Java 표준 docs에 적혀 있는 Executor graceful shutdown 구현을 설명과 함께 보실 수 있습니다)
// val scope = CoroutineScope(...)
suspend fun shutdownGracefully(timeoutMs: Long = 5000) {
// 더 이상 새 작업을 받지 않게 하고
scope.coroutineContext[Job]?.cancel() // 자식들에게 취소 신호
// 일정 시간 내 정상 종료를 대기
withTimeoutOrNull(timeoutMs) { rootJob.join() }
// 필요시 미완료 작업 강제 중단 로직 추가
}
와 같은 코드를 구현할 수 있겠죠. (구현하기 나름입니다)
역시 비슷하게 custom dispatcher를 만드는 경우도 보통은 Executors.newFixedThreadPool(n).asCoroutineDispatcher
를 사용하니 이 때 만들어지는 Executor
를 이용해 graceful shutdown을 집어 넣을 수 있습니다 ☺
또한 Executor
를 이용해 custom 스레드 풀을 직접 만들어 스레드 수를 제어하는 방법을 쓰실 수도 있고요!
아니면 원래 존재하는 Default
pool에서 개수를 격리하거나 아예 원하는 thread 수를 만들어 IO
처럼 사용할 수도 있습니다. 바로 limitedParallelism 함수인데요 공식 문서는 https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/limited-parallelism.html 에서 보실 수 있고, https://velog.io/@geronimo124/Kotlin-Coroutines-dispatchers 와 같은 블로그에서는 Default와 IO에 limitedParallelism 함수를 사용했을 때 어떤 메커니즘이 적용되는지 그림으로 쉽게 보실 수 있습니다! ☺
답변이 도움이 되었으면 좋겠습니다. 감사합니다. 🙇