Microsoft MVP성태의 닷넷 이야기
for문 안에 await가 있는 경우 질문드립니다. [링크 복사], [링크+제목 복사],
조회: 10666
글쓴 사람
한예지 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) 넵, 맞습니다.
정성태

... 46  47  48  49  50  51  52  53  54  55  56  57  58  59  [60]  ...
NoWriterDateCnt.TitleFile(s)
1153이상헌6/8/201317276죄송합니다;; 또다시 의문점이 생겨 질문드립니다. [1]
1152이상헌6/8/201316644넷두이노 플러스 펌웨어 다운로드가 안되네요;;; [1]
1151황기동6/5/201317584무선 패킷 전송관련 질문 [1]
1147이진권6/2/201317889c# 윈폼 facebook과 연동2 [1]
1144황은영5/29/201322081원격지 서버에서 COM+ 호출이 되지 않습니다. [7]
1143김선희5/28/201323007윈도우 8, 익스플로러 10, 64bit->32bit [1]
1142백지훈5/15/201317911음성인식 TTS 관련해서 질문드립니다 ㅜㅜ [2]파일 다운로드1
1141나그네5/10/201318150안녕하세요..전자세금 관련하여.. 보다보다 여기까지 왔습니다. [4]
1140이성환5/8/201318186제네릭과 배열 관련 기초 질문입니다. [11]
1139양영석5/3/201316663페이스북 로그인 관련하여.... [1]
1137김민현4/30/201316492Binary 구조체에 대한 Linq query 관련 질문입니다. [3]파일 다운로드1
1136박상영4/25/201317805ksc5601에서 지원하지않는 확장문자 찾는방법 [3]
1135sequ...4/16/201333582c# dll c++ 에서 사용하기 (event 사용) [17]
1134이성환4/12/201317920volatile 키워드 관련 질문입니다. [3]파일 다운로드1
1133서광석4/8/201320940vb.net 2010에서 배포시 문제점 확인 방법에 대한 질문드립니다 [1]
1131헤딩3/28/201323162IIS 처리 성능을 높이고 싶으면 풀의 proecss 수를 계속 늘리면 될까요? [1]
1130박재율3/27/201317521WCF 서비스가 종종 죽는데 도움좀 부탁드립니다 ㅠ [1]
1129이상민3/21/201321924안녕하세요...IE Embedded Smart Client가 Windows8에서 정상동작을 하지 않습니다. [8]
1125박홍균3/19/201315274안녕하십니까. visual studio 에서 TFS 접속 관련으로 궁금한 점 여쭤봅니다.
1124궁금이3/18/201318418IIS 서버 이중화 구성시 데이터 베이스 트랜잭션 관리에 관해서... [1]
1127궁금이3/19/201315606    답변글 [답변]: IIS 서버 이중화 구성시 데이터 베이스 트랜잭션 관리에 관해서...(정성태님 봐주세요)
1122김시현3/11/201317020 Xamarin 에 관련하여 질문드립니다. [4]
1121롬메달2/14/201317487Assembly.Load로 얻은 객체안에서 제3의 어셈블리를 참조할때의 문제입니다. [8]
1120Lyn2/12/201316947안녕하세요. 블로그에 기술관련글을 꾸준히 포스팅 하실 수 있는 원동력이 궁금합니다. [1]
1119박진영2/4/201340068닷넷 DLL 실행 에러 (보호된 메모리를 읽거나 쓰려고 했습니다..) [2]파일 다운로드1
1118C#조으다1/29/201318620공유기 관리 페이지에 접근시 윈도우 인증창이 뜹니다. [4]
... 46  47  48  49  50  51  52  53  54  55  56  57  58  59  [60]  ...