인프런 커뮤니티 질문&답변

나라바님의 프로필 이미지

작성한 질문수

2시간으로 끝내는 코루틴

9강. 코루틴과 Continuation

corutine task에 대한 질문

24.09.16 09:42 작성

·

46

1

안녕하세요! 강의 정말 잘보고 있습니다. 한 가지 질문이 있는데요. 스레드도 많이 생성될 경우 컴퓨터의 자원을 빠르게 소모하여 서버가 다운될 수 있기 때문에 스레드 풀로 관리를 할텐데 코루틴의 경우에도 코루틴 자원을 무분별하게 많이 생성을 방지하기 위해 별도의 pool 같은게 있을까요?

답변 2

0

최태현님의 프로필 이미지
최태현
지식공유자

2024. 09. 16. 16:01

안녕하세요! 나라바님! 🙂 정말 좋은 질문 감사드립니다.

아래 인프런 AI 인턴이 살짝 잘못 얘기해주는 게 있어 조금 더 정확히 답변 드려 보겠습니다.

 

강의에서도 언급 드린 것처럼

  • 결국 코루틴이 실행이 되려면 실제 스레드에 매핑되어야 합니다.

  • 또한 코루틴은 스레드와 다르게 무척 무척 가볍습니다 (수백만개가 생성되어도 괜찮아요!)

 

그렇기 때문에 코루틴을 제어하기 위한 별도의 pool을 두지는 않고,

결국 코루틴이 실행될 스레드 pool을 제어하게 됩니다.

 

결국 Dispatcher도 내부에는 스레드 풀을 갖고 있고요, 역으로 스레드 풀을 이용해 Dispatcher를 만들 수도 있는 것이 그 까닭입니다! 🙂

 

 

이는 코루틴과 유사한 가상 스레드에도 비슷하게 적용되는데요! 자바 21부터 등장한 가상 스레드의 주요 가이드 중 하나도 "스레드와 달리 pooling을 하지 않고, 그냥 사용할 것" 입니다.

 

한 줄로 요약드리면, "코루틴 pool은 없고 대신 코루틴이 실행될 스레드 pool을 만들어 관리한다" 입니다!

답변이 도움이 되었으면 좋겠습니다. 감사합니다. 🙇

나라바님의 프로필 이미지
나라바
질문자

2024. 09. 16. 17:44

명쾌하게 알려주셔서 정말 감사합니다. ㅎㅎ 명절 잘 보내세요!!

나라바님의 프로필 이미지
나라바
질문자

2024. 09. 17. 14:24

태현님 공부하다가 또 의문점이 생겨서 찾아오게 되었습니다. ㅎㅎ
보통 코루틴을 사용하지 않고 기본적인 스레드 풀만 사용을 하게 되면 코루틴 처럼 IO 스레드, main 스레드, default 스레드라는 개념이 없이 사용을 하고 있는데 결국 코루틴도 기존 스레드 풀에서 위 세개의 스레드로 나눠지게 될텐데 이 나눠지는 방식이 좀 궁금합니다!
예상하는 동작 방식은 스레드 풀에 스레드가 16개가 있다면 코루틴을 사용하여 io 스레드 선언시에 16개의 스레드 중에 1개의 스레드가 io 스레드로 될테고. 만약 여러곳 (a class, b class)에서 io 스레드를 사용하게 된다면 생성됐던 스레드에 task가 들어갈 것 같다고 생각이 듭니다! (맞는지는 모르겠지만..) 그럼 여기서 의문점이 드는것이 있는데요.

io, main, default 블록으로 선언하여 생성된 스레드를 a 스레드라고 하고 코루틴으로 인하여 생성하지 않고 다른 연산에 의해 생성된 그냥 스레드를 b라고 할 경우 코루틴의 task가 b스레드에 참여가 될 수가 있나요? 아니면 혹은 io, main, default 선언으로 인하여 생성된 스레드에만 코루틴의 task가 들어갈 수 있다고 생각하면 될까요?

최태현님의 프로필 이미지
최태현
지식공유자

2024. 09. 17. 15:05

안녕하세요 나라바님! 언제든 의문점이 생기시면 질문 주세요~~!! 👍

답변을 드리기 앞서

결국 코루틴도 기존 스레드 풀에서 위 세개의 스레드로 나눠지게 될텐데

라고 말씀해주신 부분을 정확히 짚고 넘어가야 할 것 같습니다.

코루틴이 어떤 스레드에서 실행될지는 현재 사용하고 있는 Dispatcher 에 따라 다릅니다! 그리고 Dispatcher 마다 고유한 스레드 풀을 갖고 있어요.

제가 코루틴을 Dispathcers.IO 에서 실행시킨다면, io 스레드에 코루틴이 바인딩되어 돌아가는 것이고, Dispatchers.Default 에서 실행시킨다면, default 스레드에 코루틴이 바인딩 되어 돌아가는 것이죠! 🙂

 

또한 이런 Dispatchers는 진작 고유한 스레드 풀을 갖고 있습니다. 때문에 io, main, default 블록을 만든다고 하더라도 스레드가 생성되지는 않고, 진작 존재하는 스레드에 제가 생성한 코루틴이 배정되는 방식입니다.

 

따라서

io, main, default 블록으로 선언하여 생성된 스레드를 a 스레드라고 하고 코루틴으로 인하여 생성하지 않고 다른 연산에 의해 생성된 그냥 스레드를 b라고 할 경우 코루틴의 task가 b스레드에 참여가 될 수가 있나요? 아니면 혹은 io, main, default 선언으로 인하여 생성된 스레드에만 코루틴의 task가 들어갈 수 있다고 생각하면 될까요?

에 대해 답변을 드려보면, 코루틴의 task가 어떤 스레드 (a스레드 or b스레드)에 배정될지는 해당 코루틴을 어떤 Dispatchers를 이용해 동작시키질지에 따라 다르며, 따라서 "아니면 혹은 io, main, default 선언으로 인하여 생성된 스레드에만 코루틴의 task가 들어갈 수 있다"와 유사한 것 같습니다! 🙂 (io, main, default 선언을 한다고 해서 스레드가 생성되지 않음은 위에서 말씀드렸습니다!)

 

명절에도 공부하시다니~ 고생 많으십니다. 🙂 이런 개념 자체를 잘 알아두시면, 추후에 멀티스레드, 가상 스레드와 같은 개념을 찾아 보실 때도 비슷한 측면이 많아 도움이 많이 되실 거에요!

나라바님을 응원합니다. 🔥 감사합니다! 🙏

나라바님의 프로필 이미지
나라바
질문자

2024. 09. 17. 16:57

태현님 명절인데도 답변 감사합니다. 😀 답변 주신것을 보고 질문 몇 가지만 남깁니다!!
1. 디스패처 별로 고유한 풀을 가지고 있다고 했는데 프로젝트에 스레드 풀을 설정하면 이 스레드 풀 안에서 디스패처 별로 스레드 풀을 나눈다고 생각하면 될까요? 즉 프로젝트에 설정된 스레드 풀이 A라고 한다면 이 A라는 스레드 풀을 나눠가진 디스패처 스레드 풀이 별도로 있게 될 것이고 디스패처 block 안에서 실행된건 디스패처 스레드에서 실행되고 그렇지 않을 경우에는 A스레드에서 실행되는 것이 맞을까요?!
2. 디스패처 별로 각각의 스레드 풀을 가지고 있다면 프로젝트의 성향에 따라서 디스패치 풀 size를 조정하면 좋을 것 같은데요. 예를들어서 cpu 연산이 많은 프로젝트라면 default 디스패처의 풀 사이즈를 좀 더 늘린다거나 같이요! 혹시 이런것도 가능한가요?
3. 마지막으로 디스패처가 스레드 풀을 생성할 때 어떠한 기준으로 스레드 풀 생성이 되는지도 궁금합니다! 전체 스레드 풀에서 몇 개 정도 스레드를 가져와서 스레드 풀을 만드는지와 같은..

최태현님의 프로필 이미지
최태현
지식공유자

2024. 09. 18. 17:09

하나씩 답변 드려 보겠습니다! 🙂

[1] 이 질문에서는 "프로젝트에 스레드 풀을 설정하면" 이라고 말씀해주셨는데요! 어떤 프로젝트에 어떤 스레드 풀을 설정하는 것인지 구체적으로 제가 알 수 없지만 Spring의 tomcat을 설정한다고 생각하고 답변 드려볼게요! 🙂

"이 스레드 풀 안에서 디스패처 별로 스레드 풀을 나눈다고 생각하면 될까요?"라고 질문 주신 내용을 먼저 짚어보면, 아닙니다! 우리가 tomcat을 설정하건 아니면 새로운 스레드 풀을 직접 만들건 디스패처에 존재하는 스레드 풀과는 별개 입니다. 한 애플리케이션 내에 여러 스레드 풀이 있을 수 있는 거죠.

추가로 Java 내에서도 CompletableFuture 를 사용할 경우 default thread pool인 common pool 역시 별도의 스레드 풀을 갖고 있습니다.

 

[2] 디스패처 별로 각각의 스레드 풀을 가지고 있다면 프로젝트의 성향에 따라서 디스패치 풀 size를 조정하면 좋을 것 같은데요. 예를들어서 cpu 연산이 많은 프로젝트라면 default 디스패처의 풀 사이즈를 좀 더 늘린다거나 같이요! 혹시 이런것도 가능한가요?

네네 물론 가능합니다! 이 경우 기본적으로 제공되는 IO, default Dispatcher의 스레드 풀을 늘리기 보다는 스레드풀을 이용해 새로운 디스패처를 만들지 않을까 싶네요! 아래와 같은 코드를 보신적이 있을거에요! 🙂

Executors.newSingleThreadExecutor().asCoroutineDispatcher()

다만, CPU 연산이 많은 경우 스레드 풀을 늘린다고 해서 성능이 더 좋아지지는 않습니다. 결국 물리적으로 동작할 수 있는 CPU core에는 한계가 있을텐데, 진작 CPU core를 최대치로 사용하고 있는 상황에서 스레드 개수만 늘리면 오히려 스레드 간의 context switching 비용과 core 경쟁이 발생해 성능이 떨어질 수 있습니다.

 

[3] 마지막으로 디스패처가 스레드 풀을 생성할 때 어떠한 기준으로 스레드 풀 생성이 되는지도 궁금합니다! 전체 스레드 풀에서 몇 개 정도 스레드를 가져와서 스레드 풀을 만드는지와 같

말씀드린 것처럼 전체 스레드 풀에서 몇 개의 스레드를 가져온다는 개념은 아니에요! 스레드 풀을 만든다는 것은 말 그대로 기존의 스레드 풀 혹은 main 스레드와 달리 새로운 스레드 풀을 만드는 개념입니다!

그리고 기본적으로 제공되는 default, IO dispatcher가 갖고 있는 스레드 풀 size는 다음과 같습니다.

  • default : CPU 코어 수의 2배

  • IO : max(64개, 코어 수)

 

답변 보시고 또 궁금한 것 있으시면 편하게 질문 남겨주세요~ 감사합니다! 😊

나라바님의 프로필 이미지
나라바
질문자

2024. 09. 18. 17:24

정말 이제 완벽하게 이해했습니다 ㅠㅠㅠ 진짜 감사합니다! 좋은 강의도 감사한데 이렇게 질문에 대한 답변까지 하나하나 친절히 알려주셔서 정말 감사해요!!

나라바님의 프로필 이미지
나라바
질문자

2024. 09. 24. 00:19

태현님 질문이 몇가지 또 있어서 찾아오게 되었습니다!

1. 스레드의 경우에는 컨텍스트 스위칭을 할 때 커널모드에서 실행이 될텐데요. 마찬가지로 코루틴도 스레드 기반에서 TASK가 실행이 될테고 단일 스레드에서 여러개의 TASK가 실행되면 컨텍스트 스위칭이 필요하지 않지만 만약 비동기처리나 동시성 처리를 할 때 같은 단일 스레드에서 실행이 되지 않은 경우에 결국 코루틴도 커널 모드에서 컨텍스트 스위칭이 일어나는 것으로 공부를 하면서 이해를 했는데 제가 이해한게 맞을까요?

2. 코루틴이 경량스레드로 불리는 이유가 스레드처럼 생성될때 스레드별로 스택을 가지지 않고 코루틴의 코루틴의 경우는 현재 함수의 실행상태, 로컬 변수등만 저장한다고 이해를 했는데요. 이 때 이 실행상태와 로컬 변수가 힙 메모리에 저장된다는 글과 스레드의 스택에 저장된다는 글을 봤는데 둘 중에 어떤게 맞는지도 궁금합니다! 코루틴이 컨텍스트 스위칭이 필요한거면 각각의 스레드 스택에 해당 정보들이 맞다고 생각되기는 하지만 정확하게 뭐가 맞는지 모르겠어서 질문드려요!

최태현님의 프로필 이미지
최태현
지식공유자

2024. 09. 24. 06:27

아이고~ 언제든 환영입니다~ 🙂

[1] 단일 스레드에서 실행이 된다고 하더라도 coroutine이 non blocking으로 동작하면 1번 코루틴 실행 하다가 yield -> 2번 코루틴으로 실행하다가 yield 처럼 처리 될 텐데, 그 과정에서 어쨌거나 코루틴이 바뀌긴 하니 아주 작은 context switch가 있다고 할 수 있을 것 같아요! (물론 2강에서 설명 드린 것처럼 process의 context switch나 thread의 context switch에 비하면 매우 미약하겠지만요~)

또한 단일 스레드에서 실행되지 않는 경우 결국 스레드가 CPU core에 binding 되어야 하므로 context switching이 일어나는 것이 맞습니다! 다만, 이 역시 일반적인 Thread-per-Request 구조의 200개, 500개 스레드 만큼 다수의 스레드를 사용하지는 않다보니 그 보다 적은 context switch로 볼 수 있을 것 같아요!

 

[2] 매우 흥미로운 질문인데요! 제 생각에는 heap에 저장될 것 같습니다 🙂 잘 아시겠지만 stack은 지역 변수처럼 특정 scope에서만 임시로 존재하는 값이고, (thread의 stack은 해당 thread라는 scope에서만 존재) heap은 해당 프로세스의 모든 단위에서 접근할 수 있는 값이죠! 코루틴의 경우는 10강에서 살펴봤던 것처럼 코루틴의 임시 상태를 수시로 저장할 수 있어야 하고, 그 코루틴을 수많은 스레드에 매핑해 실행시킬 수 있기 때문에.. (즉 여러 스레드를 넘나들며 접근할 수 있어야 하기에) heap이 맞을 것 같습니다!

 

감사합니다! 🙇

0

인프런 AI 인턴님의 프로필 이미지

2024. 09. 16. 09:46

안녕하세요, 인프런 AI 인턴입니다. 나라바님.

코루틴 또한 스레드와 유사하게 시스템 자원을 효율적으로 관리하기 위해 별도의 코루틴 디스패처와 코루틴 스코프를 통해 관리할 수 있습니다. 이에 대한 더 자세한 설명은 이미 여러 질문에서 다루어졌습니다. 아래 링크로 참고해 주세요.

관련 질문 URL

코루틴 dispatcher IO관련 질문
- 이 질문에서는 코루틴의 Dispatcher와 블락킹 상황에서의 처리 방법에 대해 설명하고 있습니다. Dispatchers.IO나 Dispatchers.Default 등의 디스패처를 이용해 효율적으로 코루틴을 관리할 수 있습니다.

추가 설명

코루틴을 무분별하게 많이 생성하는 경우를 방지하려면, CoroutineScopeDispatchers를 잘 활용하는 것이 중요합니다. 예를 들면, Dispatchers.Default는 CPU 집약적인 작업에 적합하며, Dispatchers.IO는 I/O 작업에 적합합니다. 이러한 디스패처를 통해 코루틴이 적절한 스레드 풀에서 실행되도록 관리할 수 있습니다.

코루틴의 경우 스레드보다는 상대적으로 가벼운 작업으로 간주되지만, 관리가 필요한 경우에는 launchasync에 적절한 스코프와 디스패처를 사용해 자원 소모를 최적화할 수 있습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.