.NET Console 프로젝트에서의 ThreadPool 낭비?

댓글이 길어지다보니 원래 질문이 무엇이었는지 살짝 길을 잃었지만서도…

저는 살짝 관점이 다른데요. 닷넷이 초기 출시부터 이런 비동기 개념을 포함해서 출발하지 않았고
이후 여러 업데이트를 통해 기능을 추가해왔다 로 생각하면 이해에 도움이 될 거 같아요.

약간 이렇게 볼 수 있을 거 같슴다.(블로그에 비동기 주제를 언급하면서 찌끄린 내용이지만 재탕…)

SynchronizationContext 의 등장 배경.

  • 닷넷 초기 버전에서는 메시지 펌핑을 위해 ISynchronizeInvoke 를 구현하여 > 사용하고 있었음. (구현 내용은 정확히 모름… 그냥 그랬다고 합니다…)
  • 닷넷 2.0 등장하고 asp.net 의 비동기 페이지가 추가되면서 ISynchronizeInvoke 이 비동기 페이지 처리에 적합하지 않은 것을 인지함.
  • 그래서 SynchronizationContext를 다시 설계하여 추가함.
    (하지만 나중에 알고 보니 완전한 기능 대체는 아니었다고… 그래서 지금도 WindowsForms 를 보면 ISynchronizeInvoke를 찾아볼 수 있다. )

원래 async / await 패러다임이나 SynchronizationContext 를 콘솔이 품고 출발한 것이 아니라는 것을 전제로
SynchronizationContext 는 원래 스레드로 돌아오기 위한 목적으로 개발되었다기 보다는
동기화 관점에서 말 그대로 동기화를 위한 기능을 추상화하는 클래스로 제공되었다. 라고 보는 게 맞을 거 같슴다.

즉, 늬들이 어떤 방식으로 스레드를 사용할지 몰라 SynchronizationContext 를 준비했어. Post() 동작은 늬들 맘대로 override 해서 사용해.
하지만 안 써도 좋아. 대신 안 쓰면 스레드 풀에서 돌린다.

SynchronizationContext 를 이용해 UI 를 사용하는 플랫폼에서 스레드 동기화 용도로 확장하여 구조를 작성한 것이 나중에 추가되었고
TAP 를 활용하는 구조에서 SynchronizationContext 는 필수요소였지만 사용하는 측에 스레드 동기화의 자유도(혹은 확장)를 제공
한 것이라는 관점으로 바라봐야하지 않을까 해요.

그런 차원에서 보자면 플랫폼별 SynchronizationContext 는 옵션같은 요소이기 때문에
앞서 언급되었던 이유로 asp.net core 에서도 빠지게 된 것이고
콘솔에서는 기본으로 null 인 것이죠.

이 얘기는 바꿔 말하면
이 실행 구조가 모든 것에 닷넷 플랫폼에 적용되는 불변의 법칙이 아니라, 필요하면 추가할 수 있는 옵션이라는 거예요.

그러니까 콘솔에서도 main 스레드를 놀지 않도록 처리하는 어떤 로직을 넣은 SynchronizationContext 를 만들어서 붙여서 실행하면
그렇게 동작한다는 겁니다.

3개의 좋아요

음… 제가 질문에 UI 스레드라고 하지 않고, 원래 스레드로 돌아가게라고 적었습니다. 콘솔앱도, aspnetcore도 UI Thread는 없으니까요.

윈폼과 WPF 환경이라면 기본적으로 UI Thread에서 돌리는거면 굳이 스레드풀이나 그런 문제가 아니라 어플리케이션 자체가 온갖 행에 걸릴텐데 그러면 당연히 안되는거죠.

말씀하신 맥락과 제 질문요지는 다른 듯하고, 정리하여 말씀하신 것들도 위 내용들에 다 나온 내용들이라서 글을 적어 주신 요지가 제가 파악이 어렵네요…

혹시 조금 더 말씀하시고자 하시는 요지를 추가 설명해주실 수 있을까요?

네네, @BigSquare 님이 링크해주신 이 글에도 말씀하신

이 내용이 있어서 이런 부분은 인지를 했습니다.

제가 낭비라고 했던 부분은

void Main() {}

이 아니라

async Task Main() {}

일 경우…

  1. 기존의 Thread가 놀아야 한다는 점
  2. 기존의 Thread에서 비동기를 호출해서 Thread Pool에서 임대한 Thread가 지속적으로 점유되어 회수되지 못하는 점.

두 가지였습니다.

이것을 낭비라고 봤던 이유는

Main Thread에서 비동기를 호출하여 Thread Pool의 Worker Thread를 가져와서 쓰는거까지는 전혀 문제가 없지만,

이후 원래 Thread인 Main Thread로 다시 복귀하지 않고 임대하여 쓰고 있는 Thread Pool의 Thread를 계속 사용하게되면 이 Thread는 Long Running이 아니기 때문에 계속해서 Thread Pool이 1개만큼의 Overhead를 계속 발생시켜야한다는 점이 낭비라는 포인트였습니다.

원래 스레드로 복귀하게 되면 스레드도 기존것을 쓰게 되고 빌려와서 쓴 것은 반납할 수 있으니까 제가 논점으로 잡았던 낭비 포인트가 사라지게 되는 점이지요.

다른 언어들도 이와 같다는 점은 다시 확인은 안 해봤지만 위에 예시로 들었던 Rust의 tokio로 이해를 하기로 했습니다. Rust도 UI 프레임워크가 제가 알기론 없어서 비동기 호출 후 아마도 원래 Thread로 안 돌아갈거같은데요…궁금하긴 해도 어차피 제가 만들고 설계한게 아닌지라 그냥 그런가보다 하고 넘어가기로 했습니다.


아 물론! 원래 스레드로 꼭 돌아가야만 리소스 낭비가 아니냐는 포인트는 아니고, 위에 글에서도 언급된 대로 프로젝트 형태마다 이런 기초 원리가 다르기 때문에 비기너들의 입장에서는 헷갈릴 수 있겠다 라는 개인적인 의견입니다.

위에서 말한대로, 1개만큼의 오버헤드라는 게 꼭 그렇게 심한 뇌절도 아닐뿐더러, 원래 스레드로 돌아가야한다는게 리소스 적인 측면에서는 중요한 점도 아니라고 생각합니다.

1개의 좋아요

아. 이제 질문의 요지를 이해했어요.

근데 원래 스레드로 돌아간다… 라는 건 애초부터 스레드 자체의 기능으로는 구현이 불가능한 것으로 알고 있어요.
UI 를 다루는 대부분의 상황은 메시지 펌프를 돌게 되고 메시지 펌프로 들어오는 작업이 Queuing 되므로
SynchronizationContext.Post() 이후 작업을 메시지 펌프로 연결하는 동작만 있으면 메시지 펌프를 수행하는 스레드에서 수행이 가능해요.
하지만 메시지 펌프가 없는 콘솔은 그게 불가능할 거 같은데요.

모든 프로세스에서 메시지 펌프를 만들어서 유지한다면 일관된 SynchronizationContext 의 상위 구현이 가능하겠지만
아마도 그건 불가능할 거 같아요.

6개의 좋아요

아하…이 글 스레드에서 또다른 지적 통찰력을 주시는 내용이었습니다.

정말 감사합니다!!

3개의 좋아요

@Vincent
해당 질문에 대해서 정성태님이 정리하여 포스팅 해주셨습니다.
닷넷: 2298. C# - Console 프로젝트에서의 await 대상으로 Main 스레드 활용하는 방법 (sysnet.pe.kr)


‘SynchronizationContext’ 내용과 ASP.NET / ASP.NET Core 에서 왜 빠졌는가 에 대해서 제 생각과 같아 개인적으로 뿌듯하군요 힣 :smiley:

5개의 좋아요

와우…환상적인 답변… 역시 @kevin13 님 감사합니다…

ㅋㅋㅋㅋ 간택 축하드립니다

2개의 좋아요

모르는 부분을 많이 배웠습니다. :grinning:

dotnet 디자인 결함이란 표현은 잘못작성했군요.원래 전달하려는 의도는 configureawait(true)가 기본값인 것을 디자인 결함이란 표현으로 잘못 작성했네요.아래 내용을 공감하는 수준입니다.

엑스퍼트간 코드 모든곳에 configureawait(false)를 붙이는 거에 대한 반감은 공감대가 형성되고 있다고 혼자 상상했나 봅니다. 모든 곳이란 표현도 조금 강한 느낌인데… 아래 블로그와 공감하는 수준입니다.

최근 닷넷에서도 그린쓰래드에 대한 논의가 있기도 했고 먼가 변화가 있으면 좋겠네요.

3개의 좋아요