Microsoft MVP성태의 닷넷 이야기
메서드 내에서 await 2번 등장할 때의 이해 [링크 복사], [링크+제목 복사]
조회: 587
글쓴 사람
강성욱 (dnr2144@gmail.com)
홈페이지
첨부 파일
 안녕하세요. 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를 해도 기존에 빌린 스레드는 반납이 되지만 또 스레드 한 개를 대여해 결국에 똑같지 않나영..?ㅣ
[손님]
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나 똑같은 거 같은 게 제 생각입니다. 빌려오는 스레드 개수도 같구요.. 하나 예시를 들어주실 수 있나요?
 
[손님]
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 메서드를 호출한 주 스레드 제어도 돌려주었고...

[손님]
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분
[강성욱] 제가 너무 어렵게 생각한 것 같네요. 귀찮게 해드려서 정말 죄송하고 다시한번 친절한 답변 감사드립니다ㅜㅜ. 첨부하신 글은 상세히 읽어보겠습니다
[손님]
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가 완료될 때까지 대기할 수밖에 없나요?

[손님]
2020-04-19 03시00분
FileStream의 ReadAsync는 운영체제와 엮인 비동기입니다. 그래서 내부적으로는 별도의 스레드가 그것을 대기하는 방식이 아닙니다. 이 부분에 대해서는 이야기가 길어지니까, 우선 제 책에서 "6.6.6 비동기 호출" 절에서 나오는 "동기 호출"과 "비동기 호출"을 다시 한번 읽어보시고 질문을 해주세요.
정성태

1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
5229fox3699/23/2019682C# 메모리맵드파일 관련 질문드립니다. [2]
5227세퉁9/23/2019736WPF Textblock 폰트 크기에 따라 글자 색이 깨지는 현상이 있습니다. [3]파일 다운로드1
5226김대훈9/23/2019764정말 황당한 경우입니다.. [2]
5223김태균9/19/2019508책 소개 링크가 7.1버전판으로 이어집니다. [1]
5222냥냥이9/14/2019907프로그래밍 논리력이 많이 부족합니다 [3]
5219티지레몬9/9/20191083c# PCB 자동화 프로그램(윈도우 폼 위주로 작업) 제작 준비 [3]
5218민성9/9/2019593안녕하세요 WPF에서 xaml 안에 다른 xaml을 넣고 싶습니다. [1]파일 다운로드1
5216WPF9/8/2019737WPF에서 XAML Islands를 사용하여 Win2D를 사용하니 그래픽 품질이 저하됩니다. [2]파일 다운로드1
5215허송세월9/5/2019725중복실행 방지 관련 문의 [2]파일 다운로드1
5214JangHun9/4/2019818[DB 테이블의 데이터 변경에 대한 알림 처리] SQL-Server말고 MySQL은 불가능하겠죠? [1]
5213진우8/31/2019799c# 람다 변수 캡쳐 문의 [2]
5212심성보8/29/20191110Clipboard내 여러개의 이미지를 PictureBox로 불러오는 문제 [2]
5211최휘철8/24/2019626CLR20r3 관련된 윈도우 오류입니다. ㅠㅠ 도와주세요. / 아래글 관련하여 관련 파일 올려 드려요^^ [1]파일 다운로드1
5210최휘철8/23/20191985CLR20r3 관련된 윈도우 오류입니다. ㅠㅠ 도와주세요. [5]
5209세퉁8/21/2019686폰트 파일 속성 값을 가져오는 방법 질문 드립니다. [2]파일 다운로드1
5208홍길동8/19/2019762DebugDiag에서 .Net의 Stack Trace를 Windbg에서는 어떻게 볼 수 있나요? [3]
5207민성8/16/2019836네 소스 전체를 올리도록 하겠습니다. [2]파일 다운로드1
5206민성8/14/2019711전 재현 가능하다고 봤는데 다시올리도록 하겠습니다. [1]
5205minyy1@hanmail.net8/14/2019706안녕하세요 .WPF ListBox시 체크박스가 있는데 체크박스에서 체크가 되었는지 알수 있는 방법이 있을까요? [1]
5204영민8/8/20191225안녕하세요 디버깅시 콘솔창을 띠어서 볼수가 없나요? [7]
5202민성8/6/2019676WPF에서 <Application.Resources에 xaml에 있는 icon 값을 저장하고 xaml에 불러다 사용하고 싶은데요 [1]
5201김대훈8/3/2019797상속시 생성자에 대해 질문드립니다 [3]
5200농상7/30/20191119foreach로 데이터 변경 [2]
5190오리다람7/20/2019880질문드립니다. [3]
5189진우7/19/20191239C# 스레드풀 코어별 실행 문의 [2]
5188황태관7/19/2019895비주얼베이직 2019 실행 할때 마다.. [3]
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...