Microsoft MVP성태의 닷넷 이야기
for문 안에 await가 있는 경우 질문드립니다. [링크 복사], [링크+제목 복사]
조회: 4009
글쓴 사람
한예지 donator
홈페이지
첨부 파일
 

선생님 오늘 마지막 질문이 될 것 같습니다!

namespace Temp
{
    class Program
    {
        private static async void SimpleTest()
        {
            int sum = 0;
            for (int index = 1; index < 4; index++)
            {
                Console.WriteLine("Code Block ##{0}", index);
                sum += await ServerCallAsync();
            }
            Console.WriteLine("Result : {0}", sum);
        }

        public static Task<int> ServerCallAsync()
        {
            return Task<int>.Run(() =>
            {
                return 5;
            });
        }

      
        static void Main(string[] args)
        {
            SimpleTest();
            Console.ReadLine();
        }
    }
}

Main Thread는 Code Block ##1 출력 후 SimpleTest() 메서드를 빠져나간다.

ServerCallAsync 메서드 내부 : 스레드 풀에서 새로운 스레드 3번을 꺼내서 5를 반환한다.
await 아래 문장들 실행 : 스레드 풀에서 새로운 스레드 4번을 꺼내서 아래 3문장을 실행한다.
sum = sum + 5;
index++; (i = 2)
Code Block ##2 출력

ServerCallAsync 메서드 내부 : 스레드 풀에서 스레드 5번을 꺼내서 5를 반환한다.
await 아래 문장들 실행 : 스레드 풀에서 새로운 스레드 6번을 꺼내서 아래 3문장을 실행한다.
sum = sum + 5;
index++; (i = 3)
Code Block ##3 출력

ServerCallAsync 메서드 내부 : 스레드 풀에서 새로운 스레드 7번을 5를 반환한다.
await 아래 문장들 실행 : 스레드 풀에서 새로운 스레드 8번을 꺼내서 아래 2문장을 실행한다.
sum = sum + 5;
Result : 15 출력

[질문 ①]
스레드 풀에 요청을 6번하는 코드 흐름이 맞나요?
정확한 이해를 위해서 스레드 풀에 호출할 때마다 매번 다른 스레드가 호출된다고 가정했습니다.

[질문 ②]
현재 제가 사용하는 컴퓨터는 코어가 2개입니다.
그래서 실제로 위에 코드를 실행시키면 스레드 3번, 4번 오직 2개만 생성해서 처리하더라구요.
이것은 CPU 코어 수만큼 Working 스레드를 만드는 것이
context switching 비용을 최소화하기 때문에
2개의 Working 스레드만 사용했다고 생각해도 되나요?








[최초 등록일: ]
[최종 수정일: 5/10/2022]


비밀번호

댓글 작성자
 



2022-05-10 10시34분
1)
await 아래 문장들 실행: 스레드 풀에서 새로운 스레드 4번을 꺼내서 아래 3문장을 실행한다.

==>

await 이후의 코드를 "스레드 풀에서 새로운 스레드"를 꺼내는 것은 I/O 호출에서 발생하는 현상입니다. 그 외의 경우에는 해당 I/O 스레드를 구동하려면 I/O 호출에서 했던 것과 같은 식의 코딩을 해야 하는데, 본문에서는 그런 경우가 아니기 때문에 그런 식의 "새로운 스레드"가 선택되는 작업은 없습니다.

위의 코드에서는 Thread.Run이 호출되고 있으며, 그 안에 전달한 코드를 실행하도록 스레드 풀의 스레드(A 스레드)를 선택하고 있는데요, 바로 그 스레드가 I/O 호출에서 선택된 스레드의 역할을 하게 됩니다. 즉, Thread.Run으로 구동된 A 스레드가 내부의 코드를 완료한 이후 await으로 분리된 코드를 실행해 가기 시작합니다.

그러다가, 다시 for 문에 의해 그 A 스레드는 Thread.Run을 호출하게 되고, 그 순간 다시 스레드 풀로부터 선택된 새로운 스레드(B 스레드)가 작업을 수행하게 됩니다. 그 순간 A 스레드는 실행할 코드가 더 이상 없으므로 스레드 풀로 반환이 됩니다.

이후는 위의 설명이 반복됩니다.

2)
위에서 보면 A 스레드가 최초 선택되고, B 스레드가 선택이 된 후 A 스레드는 풀에 반환이 됩니다. 이후 다시 B 스레드가 Task.Run을 수행하면 스레드 풀에 여유로 남게 된 A 스레드가 선택이 됩니다. 그것이 반복되기 때문에 2개의 스레드로 처리가 되는 것입니다.
정성태
2022-05-11 02시29분
[한예지] [질문 ①] 선생님 답변에 근거하면 결국 위의 코드는 스레드 풀에 스레드 요청을 3번 한 것이 맞을까요?
        (for문 안에서 ServerCallAsync() 호출한 코드를 의미)

위의 코드를 약간 수정했습니다.
ServerCallAsync → NetworkStream.ReadAsync
int sum → int received
namespace Temp
{
    class Program
    {
        private static async void SimpleTest()
        {
            int received = 0;
            for (int index = 1; index < 4; index++)
            {
                Console.WriteLine("Code Block ##{0}", index);
                received += await ns.ReadAsync(...); // ns는 NetworkStream의 객체
            }
            Console.WriteLine("Result : {0}", received);
        }
      
        static void Main(string[] args)
        {
            SimpleTest();
            Console.ReadLine();
        }
    }
}

Main Thread는 Code Block ##1 출력 후
IRP(I/O 요청 패킷)을 디바이스 드라이버의 IRP 큐에
IRP를 큐잉 작업까지 처리하고 SimpleTest() 메서드를 빠져나간다.

스레드 풀에서 새로운 스레드 A을 꺼내서 await 아래 3문장을 실행한다.
received = received + 5;
index++; (i = 2)
Code Block ##2 출력

스레드 A는 IRP(I/O 요청 패킷)을 디바이스 드라이버의 IRP 큐에
IRP를 큐잉 작업까지 처리한다.
스레드 풀에서 새로운 스레드 B를 꺼내서 await 아래 3문장을 실행한다.
received = received + 5;
index++; (i = 3)
Code Block ##3 출력

스레드 B는 IRP(I/O 요청 패킷)을 디바이스 드라이버의 IRP 큐에
IRP를 큐잉 작업까지 처리한다.
스레드 풀에서 새로운 스레드 C를 꺼내서 await 아래 2문장을 실행한다.
received = received + 5;
Result : 15 출력

[질문 ②]
코드 흐름이 맞다면 for문 안에서 NetworkStream.ReadAsync를 호출하면
스레드 풀에 요청을 3번하는 코드 흐름이 맞나요?
정확한 이해를 위해서 스레드 풀에 호출할 때마다 매번 다른 스레드가 호출된다고 가정했습니다.

[질문 ③]
// [case ①] await 이후 코드는 스레드 풀에서 새로운 쓰레드를 꺼낸다.
int received = await I/O 호출 메서드();
Console.WriteLine(received);

// [case ②] await 이후 코드는 MyAsync를 수행한 스레드가 실행한다.
int received = await MyAsync();
Console.WriteLine(received);

참고로 MyAsync 메서드 코드는 아래와 같습니다.
public static Task<int> MyAsync()
{
    return Task<int>.Run(() =>
    {
        return 5;
    });
}

결국 await 키워드 뒤에 I/O 호출 메서드가 아니라
MyAsync 메서드처럼 Task<int>를 반환하는 메서드라면
await 이후 코드가 동기적으로 실행된다고 보는 것이 맞을까요?
그러니까 Task.Run으로 구동된 스레드가 코드를 완료한 후 await으로 분리된 코드를 실행한다고 말씀하셨기 때문에
동기적으로 실행한다고 이해했는데 맞을까요?

[질문 ③] 당연히 I/O 호출 발생하는 메서드는 아래와 같은 것이겠죠?
FileStream.ReadAsync
WebClient.DownloadStringAsync,
NetworkStream.ReadAsync
NetworkStream.WriteAsync
StreamReader.ReadLineAsync
StreamWriter.WriteAsync
[guest]
2022-05-11 10시02분
1) 맞습니다. 약간 복잡한 부분을 추가해 보면, 경우에 따라 I/O 메서드는 비동기 호출이 아닌, 곧바로 제어를 반환할 때도 있습니다. 가령, Disk에서 파일을 읽어들일 때, 1바이트를 ReadAsync해도 내부적으로는 (일반적으로) 4K씩 디스크 읽기가 발생하므로 연이은 ReadAsync의 경우에는 파일 cache에 담긴 내용을 곧바로 읽어들이므로 제어를 바로 반환합니다. 그런 경우에는 스레드 풀의 스레드가 관여하지 않고, ReadAsync를 호출한 스레드가 await 이후 작업도 담당합니다.

2) 맞습니다. 그런데 "스레드 풀에 요청을 3번"한다는 것이 이 글을 읽는 분들로 하여금 오해의 소지가 있는데요. ReadAsync를 호출하는 스레드가 직접 "스레드 풀에 요청을" 하는 것은 아닙니다. I/O 동작이 완료된 후 CLR 내부의 코드에 의해 스레드 풀로부터 스레드가 하나가 불려와 await 이후의 요청을 처리하는 것입니다.

3)
case 1번의 경우, 1)번 질문과 같으므로 생략합니다.

case 2번의 경우, Task.Run으로 인해 CLR Thread Pool에서 선택된 스레드 하나가 "return 5" 코드를 수행하고 await 이후의 "Console.WriteLine(...)" 코드도 수행합니다. 따라서, Task.Run으로 빌려진 스레드 입장에서는 해당 코드를 동기적으로 수행한다고 봐야 하고, 애당초 MyAsync를 수행하던 스레드 입장에서는 이후의 과정이 (Task.Run에 의해) 비동기로 수행한다고 보면 됩니다. (사실, 여기서도 복잡한 경우가 하나 더 있는데, 이것은 나중에 별도의 글로 정리해 보겠습니다.)

4) 넵, 맞습니다.
정성태

1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
5647장성욱4/7/20223902코어지정 CPU사용률 관련 질문 [1]
5646서형주4/6/20223913List에 여러개의 class 객체를 만들어 넣을때, 객체의 method들도 같이 생성되어 메모리를 차지하나요? [1]
5645김인태4/6/20223557윈도우즈 서버의 AD 계정 생성 조건이 있을까요? [1]
5644ㅇㅇ4/6/20224314c# 프로그램을 이용하여 리눅스상에 파일 생성이 가능한가요? [1]
5643유필재4/5/20224184TCP클라이언트 연결 및 통신관련하여 문의드려요 [1]
5642차가워4/4/20224434UdpClient 패킷 수신 문의 [4]
5641장성욱4/4/20224846코어 할당 및 cpu 부하테스트 질문 [7]
5640icoo...4/4/20224373웹가든에서 메모리 동적 업데이트 방법 [1]
5639차가워4/4/20224283c++ 서버 c# 클라이언트 호환 문의 [1]
5638초급4/3/20224584c# sql server 연동 [1]
5637따봉이4/1/20224849Winform Form Load 후 자동 캡쳐관련 [1]파일 다운로드1
5636김철순3/31/20224832WPF에서 Richtext의 View 문의 [5]
5635guest3/30/20224857안정적인 pinning이 가능하네요. [3]파일 다운로드1
5633꿀주세요3/30/20224505선생님 마우스 클릭이벤트 질문이 있습니다. [4]
5632김현수3/30/20224836Remote Desktop으로 접속시 WPF UI 가 다시 그려지는 이벤트를 막을 수 없을까요? [3]
5631김기헌3/24/20224460WPF 컨트롤의 그래픽 처리관련 질문드립니다 [2]파일 다운로드1
5630장성욱3/24/20224294로깅관련 질문입니다. [2]
5629감사합니...3/23/20224554함수에서 예외가 발생하면 try ~ catch처리기를 찾을 때 까지 상위 함수로 계속 올라가나요? [2]
5628홍길동3/23/20225190질문드립니다. [2]파일 다운로드1
5626연준혁3/21/20224420안녕하세요. [3]
5625jaew...3/18/20225356c# 8.0 도서를 구입한 사람입니다. [1]
5624초보자3/17/20224320람다 캡처 관련 문의 [2]
5623한예지 donator3/15/20224696인터프리터 원리가 궁금합니다. [4]
5622김민아3/8/20224632const와 readonly의 명확한 차이가 이게 맞나요? [2]
5621장성욱3/8/20224437c# 로그 관련 질문 [1]
5620김민아3/7/20224469안녕하세요 비관리 객체를 반환하는 메소드 호출 시 궁금한 점이 있어서 질문드립니다 [2]
1  2  3  4  5  6  7  8  9  10  11  [12]  13  14  15  ...