Microsoft MVP성태의 닷넷 이야기
메서드 내에서 await 2번 등장할 때의 이해 [링크 복사], [링크+제목 복사]
조회: 8667
글쓴 사람
강성욱 (dnr2144 at gmail.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

 안녕하세요. async/await를 공부하는 초보입니다.
 정성태님이 말씀하신 대로 한 게시물당 한 개의 질문으로 제한하겠습니다.
 질문) 한 메서드 내에서 await를 사용하기 시작한 시점부터 그 이후의 코드는 전부 별도로 처리 될 텐데 굳이 await를 한 메서드 내에서 두 번 쓰는 이유를 모르겠습니다.
 가장 이해가 안 되는 부분인데 p682쪽에 ns.ReadAsync가 끝나 야 그 밑에 코드가 실행되는데 굳이 await ns.WriteAsync를 사용할 이유가 있나요? 즉, 메서드 내에서 await를 쓰면 그 전 명령어는 호출 스레드에서 실행되다가 await구문 이후로는 호출 스레드는 반납되고 정성태님이 말씀하신 스레드풀의 스레드나 Task나 같은 별도의 스레드로 그 후 명령이 실행되고 그 이후는 순차적으로 실행될 텐데 말이죠.. (await 이후에 스레드 풀 스레드가 반납되는 게 아니라 메서드 종료 후에 원래 스레드 흐름으로 복귀하는 거로 저는 알고 있습니다)이렇게 생각한 이유는 p677 예문에 주석으로 await이후의 코드는 c# 컴파일러가 분리해 ReadAsync 비동기 호출이 완료된 후 호출이라고 쓰여 있어서요... 정리하자면 ns.ReadAsync가 종료되고 그 이후의 명령이 순차적으로 실행되고, 다시 ns.ReadAsync가 종료되고 ns.Close()가 실행되는데 첫 번째 await는 호출 스레드 제어를 반납하기 위해서(첫 번째는 별도의 스레드에서 작업하기 위해서) 그럴 수 있지만 두 번째 await는 의미 없는 거 아닌가? 가 제 궁금증입니다...
 항상 소중한 답변 감사드립니다...


[연관 글]






[최초 등록일: ]
[최종 수정일: 3/31/2020]


비밀번호

댓글 작성자
 



2020-04-01 01시29분
첫 번째 await에서 호출 이후의 코드를 수행하기 위해 스레드 풀로부터 Thread를 빌려 오는 경우로 한해서 설명을 해보겠습니다.

어쨌든 스레드 풀의 스레드도 마찬가지로 자원입니다. 그걸 빌렸으면 최대한 빨리 다시 스레드 풀로 돌려보내는 것이 스레드 사용률을 높이는 방법인데요. 만약 두 번째 WriteAsync를 하지 않으면 그 "쓰기" 작업이 완료될 때까지 빌려온 스레드는 다른 일을 하지 않고 대기하게 됩니다. 따라서 그런 대기를 없애기 위해 Write에서도 Async 처리를 하는 것입니다.

예를 들어, Read에 5초 Write에 5초가 각각 걸린다고 하면 만약 저 작업을 동기로 하면 "10초의 시간 동안 스레드 한 개"가 점유되는 것입니다. 따라서 만약 10초 내에 100개의 요청이 있다면 100개의 스레드가 생성되어야만 정상적으로 요청을 받아들일 수 있습니다.

반면, Read만 async로 하고 Write를 동기로 호출하게 되면 스레드 풀로부터 빌려온 스레드 1개가 Write로 인해 5초 동안 점유가 됩니다. 따라서 이런 경우에는 5초 내에 100개의 요청이 있다면 마찬가지로 100개의 스레드가 생성되어야만 합니다.

마지막으로 Read와 Write 모두 비동기로 호출하면, 스레드 풀로부터 빌려온 스레드 1개는 Read 이후의 코드를 처리하다가 Write를 만나면서 스레드 풀에 반환이 됩니다. 반환된 스레드 풀은 다른 요청을 처리하는데 사용할 수도 있고, 이후 Write 동작이 완료돼서 그 이후의 코드를 수행하기 위해 다시 대여할 수도 있습니다.

읽어보시고, 또 이해가 안 되시면 다시 질문을 해주세요.
정성태
2020-04-06 09시14분
[강성욱] 답변 감사드립니다. 이해가 얼핏 되지만 한 가지 의문점이 있습니다. 제가 알기로는 한 메서드 내에서 await 이후의 코드는 전부 컴파일러가 별도로 비동기로 처리하는 걸로 알고 있습니다. 즉, 답변 주신 첫 번째 경우에 스레드 풀로부터 빌려온 스레드가 별도의 await가 없다면 놀지 않고 write까지 담당하는 것 아닌가요? "빌려온 스레드는 다른 일을 하지 않고 대기하게 됩니다" 라는 문장이 잘 이해되지 않습니다. 제가 책에서 공부한 내용으로는 한 메서드 내에서 await가 나온 이후에 별도의 추가 await가 없으면 await 이후의 명령은 비동기로 컴파일러가 알아서 처리해주는 걸로 알고 있습니다.
 다시 정리하자면 답변주신 내용 중, WriteAsync를 해야 ReadAsync 할 때 빌린 스레드를 반납하는 건 알겠는데 WriteAsync를 해도 기존에 빌린 스레드는 반납이 되지만 또 스레드 한 개를 대여해 결국에 똑같지 않나영..?ㅣ
[guest]
2020-04-07 01시04분
넵, 빌려온 스레드가 별도의 await이 없다면 그대로 이후의 모든 코드를 동기로 처리합니다.

그렇기 때문에 Write 동기 코드를 호출했을 때 운영체제가 Write에 5초가 걸린다면, 그 사이 해당 스레드는 다른 일을 못하고 대기하게 되는 것입니다. Write 메서드가 끝나기만을 기다리는 것이기 때문에 해당 스레드는 다른 작업 용도로 사용할 수 없으므로 스레드 가용성이 나빠지는 것입니다.

그리고 await가 나온 이후의 모든 코드 하나하나를 컴파일러가 비동기로 실행해 주는 것은 아닙니다.
await가 나오면, await를 호출한 스레드는 대상 메서드가 끝나기를 기다리지 않고 스레드 풀에 반환됩니다. 해당 Async 메서드 수행이 끝났을 때, 스레드 풀로부터 자유 스레드를 하나 대여 받아서 그 이후의 코드를 다시 수행하기 시작합니다.

그러다가 다시 await 호출을 만나면 위의 작업이 반복되는 것입니다.

--------------

마지막 질문의 경우, 전체적으로 봤을 때는 스레드 1개를 계속 쓰는 것은 맞지만, 해당 스레드가 Write 호출 이후 그 동작이 완료될 때까지의 순간에는 다른 작업을 할 수 있다는 차이점이 있습니다. WriteAsync를 했기 때문에 그 메서드를 await으로 호출한 스레드는 잠시 다른 작업을 할 수 있는 반면, Write 동기 메서드를 호출하게 되면 그 스레드는 다른 작업을 할 수가 없습니다.
정성태
2020-04-07 08시54분
[강성욱] 꾸준한 답변 정말 감사합니다... 헌데, 마지막에 언급하신 내용이 잘 이해가 가지 않습니다...

"해당 스레드가 Write 호출 이후 그 동작이 완료될 때까지의 순간에는 다른 작업을 할 수 있다는 차이점이 있습니다. WriteAsync를 했기 때문에 그 메서드를 await으로 호출한 스레드는 잠시 다른 작업을 할 수 있는 반면, Write 동기 메서드를 호출하게 되면 그 스레드는 다른 작업을 할 수가 없습니다"


private static async void ProcessTcpClient(TcpClient client){
   NetworkStream ns = client.GetStream();
   byte[] buffer = new byte[1024];
   int received = await ns.ReadAsync(buffer, 0, buffer.Length);
   string txt = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
   byte[] sendBuffer = Encosing.UTF8.GetBytes("Hello :" + txt);
   await ns.WriteAsync(sendBuffer, 0, sendBuffer.Length);
   ns.Close();
}
 제가 처음에 질문한 예제 682페이지의 상황은 메인스레드가 async 메서드를 호출하고 async 메서드 내부에는 await ns.ReadAsync가 등장하고 그 이후에 await ns.WriteAsync가 등장하고 있습니다.
 제가 이해하고 있는 스레드 흐름은 다음과 같습니다.
1) 메인 스레드가 ns.ReadAsync"까지" 실행시킨 후 메인 스레드 반납.
2) 스레드 풀로부터 자유 스레드를 빌려와 await ns.WriteAsync"까지" 실행. 메인 스레드는 계속 별도의 흐름으로 진행 중.
3) await ns.WriteAsync"까지" ns.ReadAsync때 빌려온 스레드가 실행시키고 난 후 스레드 풀로 반납되고 다시 스레드 풀로부터 새로운 스레드 빌려와서 await.ns.WriteAsync 이후 명령 실행.
 여기서 WriteAsync를 호출한 스레드는 await ns.ReadAsync를 할 때 스레드 풀로부터 빌려온 스레드일 텐데 위 코드에서는 WriteAsync이 완료될 때까지 별로 다른 작업이랄 만한 게 없지 않나영? 위 코드에서는 이미 ns.ReadAsync로 메인 스레드를 다시 되돌려주었기 때문에 Write나 WriteAsync나 똑같은 거 같은 게 제 생각입니다. 빌려오는 스레드 개수도 같구요.. 하나 예시를 들어주실 수 있나요?
 
[guest]
2020-04-08 09시20분
질문을 정리해 보면, 그러니까 "소켓"에서의 Write 동작이 동기로 하든 비동기로 하든 호출 시 바로 끝나는데 그걸 왜 굳이 비동기로 호출하냐는 건가요?

일단, "소켓"에서의 Write가 빨리 끝나든 안 끝나든 만약에 Write 동작에 5초가 걸린다면 거기에서도 await을 써야 한다는 것까지는 이해가 된 건가요?
정성태
2020-04-08 08시21분
[강성욱] 일단, "소켓"에서의 Write가 빨리 끝나든 안 끝나든 만약에 Write 동작에 5초가 걸린다면 거기에서도 await을 써야 한다는 것까지는 이해가 된 건가요?

 아닙니다. 빨리 끝나는 것은 제가 말한 포인트가 아닙니다.
 그리고 마지막에 왜 await를 써야하는지도 이해가 안 됩니다. 제가 가장 이해가 안 되는 부분은 저자님이 말씀하신

"만약 두 번째 WriteAsync를 하지 않으면 그 "쓰기" 작업이 완료될 때까지 빌려온 스레드는 다른 일을 하지 않고 대기하게 됩니다. 따라서 그런 대기를 없애기 위해 Write에서도 Async 처리를 하는 것입니다."

에서 빌려온 스레드는 대기하게 된다는 표현입니다. 제가 현재까지 이해한 스레드 흐름입니다. 이것부터 확인이 필요할 것 같습니다.

1. await ns.ReadAsync를 ProcessTcpClient를 호출한 스레드(주 스레드)가 실행'까지' 시키고 호출 스레드는 제어를 반납한다.(까지에 주목해주세요. 헷갈리는 부분입니다)

2. await ns.ReadAsync 이후의 코드는 ThreadPool로부터 빌려온 스레드가 실행. 즉 현재 두 가지 스레드 흐름이 있음.

3. 계속 빌려온 스레드 흐름에서 명령이 처리되다가 await ns.WriteAsync을 만나면 ns.WriteAsync'까지'는 빌려온 스레드가 처리하고 그 이후에 이전까지 명령을 처리하던 빌려온 스레드는 스레드풀에 반납된다.

4. await ns.WriteAsync가 끝나고 빌려온 스레드는 반납되고 또다시 스레드풀로부터 스레드 한 개를 빌려와서 ns.Close()를 수행한다(현재 스레드는 주 스레드와 빌려온 스레드 총 2개)

5.ProcessTcpClient 메서드가 최종 종료되면 빌려온 스레드는 스레드풀로 반납되고, 스레드는 주 스레드 한 개만 남개된다.

반면 await ns.WriteAsync를 안 하고 ns.Write만 하게 될 시 제가 이해한 상황입니다.

1. 주 스레드가 ProcessTcpClient를 호출. 주 스레드가 await ns.ReadAsync'까지' 실행.

2. ReadAsync가 마치고 나면 주 스레드는 반납되고(ProcessTcpClient 메서드 이하의 명령을 별도로 수행) 스레드풀로부터 스레드 1개를 대여해 그 이후 명령실행. 현재 스레드는 주 스레드와 대여한 스레드 2개임.

3. 빌려온 스레드가 await가 안 붙은 ns.Write를 만나면 그 메서드른 실행한다 놀지 않고 ns.Write메서드를 실행한다. 여기서 아무일도 하지 않는 표현이 이해가 잘 안 간다...

제가 이해한 바는 위와 같습니다. 질문이 처음으로 돌아간 느낌이지만 두 번째 await는 써도 되고 안 써도 될 것 같다는 게 여전한 제 생각입니다. 스레드도 똑같이 2개이고 제가 봤을 땐 ns.Write한다고 논다는 느낌도 안 들고 이미 첫 번째 await때 ProcessTcpClient 메서드를 호출한 주 스레드 제어도 돌려주었고...

[guest]
2020-04-09 09시11분
잘 이해하신 그대로입니다.

두 번째의 3번 상황에서 "여기서 아무일도 하지 않는" 다는 것은 해당 스레드가 Write 동작이 끝날 때까지 멈춰 있다는 것입니다. (단순하게 그 순간을 Thread.Sleep을 호출한 것으로 이해해도 됩니다.) 만약 WriteAsync를 했으면 스레드 풀로 돌아가서 다른 작업에 또 사용될 수 있는데 Write를 호출하는 경우라면 스레드 풀에 돌아가지 못하고 Write가 완료될 때까지 기다리게 됩니다.

그런데, 이번엔 제가 궁금해지네요. ^^ 그렇다면 왜 첫 번째 시나리오는 이해가 된다는 건가요? 메인 스레드가 Read 동작이 끝날 때까지 멈춰 있는 것을 피하기 위해 ReadAsync를 호출하는 것인데, 그것과 동일하게 두 번째 시나리오에서도 대상만 스레드 풀의 스레드로 달라질 뿐 그냥 그것을 Main 스레드로 이해해도 상관 없습니다.

------------

실제로 UI가 있는 Windows Forms/WPF의 경우에는 await 이후의 코드를 다시 Main 스레드가 다른 일이 끝나서 할 작업이 없을 때 Async 동작이 끝난 것을 이어받아 실행하는 것이 일반적입니다. 관련해서 다음의 글을 읽어보시는 것도 도움이 될 것입니다.

비동기 메서드 내에서 await 시 ConfigureAwait 호출 의미
; https://www.sysnet.pe.kr/2/0/11418
정성태
2020-04-09 02시29분
[강성욱] 제가 너무 어렵게 생각한 것 같네요. 귀찮게 해드려서 정말 죄송하고 다시한번 친절한 답변 감사드립니다ㅜㅜ. 첨부하신 글은 상세히 읽어보겠습니다
[guest]
2020-04-09 02시51분
@강성욱 혹시나 이해가 안 되는 부분이 있으면 또 질문해 주세요. ^^
정성태
2020-04-18 07시58분
[강성욱] 공부하다가 한 가지 더 의문이 생겨 여쭤보고 싶습니다. 제가 첨부한 코드에서 ReadAsync의 정의는 다음과 같습니다. Task<int> FileStream.ReadAsync(byte[], int, int);
이 함수를 직접 들여다볼 수는 없으나 추측을 해보면, ReadAsync의 내부는 Task로 별도의 스레드를 사용해 디스크로부터 Read를 모두 완료할 때까지 대기를 하고 있을 거라고 생각합니다. (맞지요?ㅠ) 그럼 이렇게 디스크와 IO관련 작업을 할 때 스레드 흐름이 1개든 2개든, 어쩔 수 없이 ReadAsync 내에서 사용하는 Task의 스레드같이 하나의 스레드는 Read 혹은 Write가 완료될 때까지 대기할 수밖에 없나요?
[guest]
2020-04-19 03시00분
FileStream의 ReadAsync는 운영체제와 엮인 비동기입니다. 그래서 내부적으로는 별도의 스레드가 그것을 대기하는 방식이 아닙니다. 이 부분에 대해서는 이야기가 길어지니까, 우선 제 책에서 "6.6.6 비동기 호출" 절에서 나오는 "동기 호출"과 "비동기 호출"을 다시 한번 읽어보시고 질문을 해주세요.

업데이트: 2020-0520) 아래의 글을 참고하세요.

C# - async/await 그리고 스레드 (4) 비동기 I/O 재현
; https://www.sysnet.pe.kr/2/0/13062
정성태

1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
5752푸헐11/15/20223421app.config 에 connectionStrings를 aspnet_regiis로 enctyption [4]
5751차가워11/8/20224218vs2022 preview net7 AOT 콘솔 실행 성능 [7]
5749차가워11/4/20223629전처리 지시문 #if DEBUG 배포시 실행 여부 [1]
5748김기헌11/3/20223949안녕하세요 선생님 싱글톤 패턴을 꼭 이렇게 사용해야 하나요? [6]
5747김기헌11/2/20223328안녕하세요 선생님 네트워크 관련 용어 중 IP 주소가 왜 논리적 주소라고 표현되는 건가요? [2]
5746물냉면이...11/2/20223513서로 다른 클래스에 있는 동일 함수의 일괄 호출 방법에 대해 궁금합니다. [3]
5745흰털너부리11/1/20223869.net core web api 사용 제한에 관한 질문 입니다. [2]
5744차가워10/31/20224257윈폼 Console.WriteLine(); 연산 문의 [1]
5743흰털너부리10/27/20223590reflection, static, override 질문입니다. [1]
5742차가워10/27/20223480하나의 socket에 여러 스레드가 접근 하는 경우 [1]
5741조호상10/27/20223740OpenCVSharp4 구현 가능 문의 [1]
5740혜성10/26/20224586Visual Studio 2022 C# 콘솔 프로그램 기본 코드 변경된 이유는 무엇인가요? [2]
5739슬픈단잠10/25/20223575조언 주신 방법으로 해봤으나, 여전히 어디가 문제인지 파악을 못했습니다. [2]
5738슬픈단잠10/24/20223980C# 마샬링 관련 질문입니다. [2]
5737감사합니...10/20/20224631찾은 어셈블리의 매니페스트 정의와 어셈블리 참조가 일치하지 않습니다. [8]파일 다운로드1
5736초보 개...10/19/20223637c# winforms 프로그램을 setup 파일로 배포 시, 설정 문의 [2]
5735농상10/17/20224273싱글톤 공부중 질문이 있습니다. [8]
5734mins10/14/20224020델파이 dll을 c#에서 사용하기 관련해서 포인트 관련이라 질문을 올립니다. [2]
5733김경환10/12/20224043선생님 질문하나만드리겠습니다. [1]
5732kss10/8/20224323c# socket.poll 버그인가요? [2]파일 다운로드1
5731kss10/8/20223885c# socket.poll 버그인가요? [3]
5730김재환10/7/20224382WPF에서 디스플레이 배율이 100%가 아닌 경우, Window의 포지션 정보가 부정확해지는 문제 [2]
5729김기헌10/2/20224062안녕하세요 선생님 뮤텍스 관련 질문 드립니다 [2]
5728김경환9/29/20224066그리드뷰관련마지막질문하나드리겠습니다선생님 [5]파일 다운로드1
5727김경환9/26/20223975c# 윈폼 tcp/ip 기반 데이터그리드뷰질문하나드리겟습니다 [3]
5726양승조9/22/20224699C# dll 과 C++ 간 배열 전달. SafeArray [10]파일 다운로드1
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...