성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>async/await 사용 시 hang 문제가 발생하는 경우</h1> <p> 게임 코디 웹 사이트에 재미있는 이야기가 올라왔군요. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 서버프로그래머인데 데드락이 왜 걸리는지 모르겠어요 ; <a target='tab' href='http://www.gamecodi.com/board/zboard-id-GAMECODI_Talkdev-no-2408-z-10.htm'>http://www.gamecodi.com/board/zboard-id-GAMECODI_Talkdev-no-2408-z-10.htm</a> </pre> <br /> 덧글에 나오지만, 위의 질문은 다음의 글을 근간으로 하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Don't Block on Async Code ; <a target='tab' href='http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html'>http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html</a> </pre> <br /> 자, 그럼 이 문제를 쉽게 풀어 볼까요? ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 우선 위의 글에 따라 문제를 재현해 보겠습니다. ^^ 간단하게 .NET 4.5 대상의 Windows Forms 프로젝트를 만들고 아래와 같이 코딩을 해주면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication3 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { var textTask = GetHtmlTextAsync(); this.Text = <a target='tab' href='https://www.sysnet.pe.kr/2/0/13572#SetOnInvokeMres'>textTask.Result</a>; } public static async Task<string> GetHtmlTextAsync() { var client = new TestClass(); string result = await client.GetTextAsync(); return result; } public class TestClass { public Task<string> GetTextAsync() { return Task.Factory.StartNew( () => { Thread.Sleep(5000); return "Hello World"; }); } } } } </pre> <br /> 이렇게 만들고 실행하면 Form1_Load의 "this.Text = textTask.Result;" 라인에서 실행이 정지되어 버립니다. (BP 걸고 직접 확인해 보세요. ^^)<br /> <br /> 일단, 왜 블록킹 현상이 발생하는지 액면 그대로 놓고 보면 다음과 같이 설명될 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 1. UI Thread가 Form1_Load를 실행 1.1. UI Thread가 GetHtmlTextAsync 메서드를 실행 1.1.1 UI Thread가 TestClass.GetTextAsync 메서드를 비동기로 실행 1.2. UI Thread는 textTask 인스턴스의 get_Result 프로퍼티를 호출. 내부적으로 Task 타입의 Result는 Thread.Join을 동반하므로 비동기로 호출한 GetTextAsync를 실행한 스레드가 종료될 때까지 대기 2. ThreadPool의 자유 스레드는 TestClass.GetTextAsync 메서드를 실행 2.1. 메서드 실행 후 텍스트를 반환. 2.2. await 처리로 인해 분리된 "return result;" 코드를 UI Thread에서 실행하도록 동기적으로 디스패칭 2.3. 동기식으로 디스패칭했기 때문에 UI Thread에서 처리를 해줄 때까지 현재 스레드는 블록됨 </pre> <br /> 어떤 식인지 이해되시나요? "2.3" 단계에서 ThreadPool의 스레드가 UI Thread에서 처리할 "return result;" 코드를 delegate로 동기식으로 전달했는데, 그 순간의 UI Thread는 ThreadPool의 스레드가 완료될 때까지 기다리는 Thread.Join을 호출했기 때문에 서로 무한 대기 상태에 빠진 것입니다. (정확히 Thread.Join은 아닐 수 있습니다. 가령 EventWaitHandle로 처리했을 수도 있는데 그리 중요한 것은 아니니 그냥 개념만 그렇다고 이해해 주시면 되겠습니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 조금 더 자세하게 설명을 해볼까요? ^^ 그럼 이번에는 위의 async/await과 동등한 효과를 가진 코드를 Thread를 이용해 만들어 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication2 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { Thread thread = new Thread(GetHtmlTextFunc); thread.Start(); thread.Join(); // 역시 hang 현상 발생 } private void GetHtmlTextFunc(object obj) { var client = new TestClass(); string result = client.GetText(); this.Invoke( (Action)(() => { this.Text = result; }) ); } public class TestClass { public string GetText() { Thread.Sleep(5000); return "Hello World"; } } } } </pre> <br /> 역시 위의 코드가 실행되는 순서를 한번 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 1. UI Thread는 Form1_Load를 실행하고 스레드를 실행한 후 Thread.Join으로 해당 스레드가 종료할 때까지 대기 2. 새로운 스레드는 GetText 메서드를 실행하고, "this.Text = result" 코드를 UI Thread 측에서 실행할 수 있도록 this.Invoke 동기 메서드를 호출. </pre> <br /> 보시면 당연히 블록킹이 될 수밖에 없는 구조를 가지고 있습니다.<br /> <br /> 그럼 여기서 한 가지 의문이 생깁니다. 왜 async/await은 자동적으로 this.Invoke와 동일한 효과를 갖는 코드로 처리하느냐입니다. 일반적인 비동기 호출이라면 async/await으로 분리된 "return result;" 코드는 그대로 ThreadPool 측의 스레드에서 마저 호출되어야만 하는 것이 정상적입니다. 그런데, async/awiat은 굳이 시키지도 않은 UI Thread에 해당 코드를 실어 보내는 처리를 하고 있는 것입니다.<br /> <br /> 이 문제를 정확히 이해하려면 약간의 역사적인 문제를 알아야 합니다.<br /> <br /> 일반적으로 UI 요소는 그것을 생성해 낸 스레드에서 접근해야 한다는 제약이 있습니다. (이것은 윈도우 운영체제만의 제약이 아닙니다. iOS도 그렇고 안드로이드까지 모두 이런 제약이 있습니다.) 문제는 닷넷이 나오기 전 C/C++ 프로그래머들이 개발했던 윈도우 프로그램에서는 그런 제약이 없었다는 점입니다. 사실 제약이 없었다기 보다는 문제는 있었지만 표면화되지 않았을 뿐입니다. (C/C++에서 다중 스레드를 이용해 ListBox에 아이템을 채우면 설령 이상하게 채워질지언정 못 채우는 것은 아닙니다.)<br /> <br /> 세월이 지나 닷넷 프레임워크가 나오면서 닷넷도 처음에는 이런 정책을 그대로 가져갔습니다. 초기 윈폼 프로젝트는 다중 스레드에서 ListBox에 값을 채우는 것이 가능했지만 Virtual Machine 성격의 닷넷은 C/C++보다 성능이 낮았기 때문에 항목이 이상하게 채워지는 문제가 두드러졌습니다. 그래서, 초기 닷넷 관련 포럼에 보면 그런 희한한 문제를 보고하는 게시물이 심심치 않게 등장했습니다.<br /> <br /> 그로 인한 문제가 많아지면서 마이크로소프트는 UI 요소를 생성하지 않은 다른 스레드에서 해당 UI 요소를 접근하는 경우 명시적인 예외를 발생하도록 변경했습니다. (제가 보기에는, 이로 인해 닷넷 포럼이 더 질문이 많아지지 않았나 생각이 됩니다. ^^)<br /> <br /> 어쨌든, UI 요소를 다른 스레드에서 건드리는 것은 심심치 않게 발생하면서도 여간 신경쓰이는 일이 아닐 수 없었던 것입니다.<br /> <br /> 이 문제를 좀 더 쉽게 해결할 수 있도록 마이크로소프트는 SynchronizationContext 타입을 만들었습니다. 이 타입은 WinForm의 경우 UI Thread를 대표하고 ASP.NET의 경우 각각의 요청을 처리하는 스레드를 대표합니다.<br /> <br /> 사실 SynchronizationContext가 없었다면 async/await은 굳이 UI Thread에 코드를 실어서 실행하려고 하지 않았을 수 있습니다. 혹은 사용될 때마다 await 이후의 코드에 UI 코드가 있을 때마다 그것을 안전하게 접근할 수 있는 스레드를 명시하기 위해 별도의 장치를 마련했어야 할 것입니다. 하지만 SynchronizationContext가 있기 때문에 UI 요소를 생성한 스레드를 전혀 모르고도 안전하게 UI 스레드에 태워서 자연스럽게 실행할 수 있게 된 것입니다.<br /> <br /> 이렇게 정책을 정한 데에는 약간의 계산이 있었을 것입니다. 사실 Windows Forms 응용 프로그램을 만든다면 대부분의 경우 UI 요소를 접근하게 될 것이므로 마이크로소프트는 별도의 스레드에서 UI를 접근하는 바람에 예외가 발생하는 문제로 초보자들이 당황하기 보다는 그냥 디폴트로 윈도우 폼에서 실행되는 모든 async/await의 호출은 완료 후의 코드를 UI 스레드에 실어서 보내도록 디자인했을 것입니다. 만약 그렇게 하지 않았다면 어떻게 되었을지 상상해 보는 것도 재미있을 텐데요. 대충 아래와 같이 코드가 될지도 모를 일입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public static async Task<string> GetHtmlTextAsync() { var client = new TestClass(); string result = await client.GetTextAsync(); // await으로 만들었기 때문에, // 이후의 코드는 UI Thread가 아닌 별도의 스레드에서 실행되므로 this.Invoke( // 이렇게 UI Thread에 태우는 코드로 변경해야 할까요? (Action)(() => { return result; }) ); } </pre> <br /> 그냥 봐도 이건 더 말이 안되는 코드입니다.<br /> <br /> 결국, 마이크로소프트는 UI Thread에 태우기로 결정했기 때문에 이번 글의 내용과 같은 문제가 부수적으로 발생하는 것입니다.<br /> <br /> 그렇다면, UI 요소를 갖는 프로그램은 이해할 수 있겠는데 왜??? ASP.NET Web API에서 그런 현상이 있는지 궁금하실 텐데요. Web API의 실행 과정이 결국 전체적인 입장에서는 동기 방식으로 동작함을 인지한다면 그리 이상할 것도 없는 문제입니다.<br /> <br /> 웹 브라우저에서 ASP.NET에 요청을 했는데 그 요청을 받은 ThreadPool의 스레드가 내부적으로 (가령, DB로부터 데이터를 구하는) 비동기 호출을 하고는 이후의 처리를 계속 진행하면 결국 HTTP 응답은 원치 않는 결과를 반환할 수 있습니다. 즉, ThreadPool의 스레드는 비동기 호출을 했지만 그 결과를 어떤 식으로든지 반드시 받아서 그것을 포함하여 HTTP 응답을 해야하는 것입니다. 이 때문에 async/await은 현재 자신의 비동기 처리를 유발시킨 스레드를 알아야 하고 그 스레드에 결과를 전달해야 하기 때문에 유사한 문제가 발생하는 것입니다.<br /> <br /> 쉽게 설명한다고 하긴 했는데, 혹시 더 혼란스러워진 것은 아닌가 걱정이 되는군요. ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 위의 글을 읽으면서 왜??? this.BeginInvoke가 아닌 this.Invoke 식으로 처리했을까 하는 의문을 가지신 분이 계신가요? ^^ 즉, SynchronizationContext 타입의 경우 Send가 아닌 Post 메서드로 처리했다면 아마 이런 식의 hang 문제는 없었을 것입니다.<br /> <br /> 사실, 저도 이 부분에 대해서는 왜 마이크로소프트가 Invoke 유를 고집했는지 궁금합니다. 어쨌든 마이크로소프트는 this.Invoke를 선택했고 이 때문에 지금의 문제가 발생한 것입니다. 이로써 마이크로소프트는 UI Thread가 아닌, 비동기 실행을 위해 사용되었던 ThreadPool의 스레드에서 그대로 await 분리 코드를 처리할 수 있는 옵션을 마련하게 되었고, 바로 그것이 <a target='tab' href='http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html'>Don't Block on Async Code</a> 글에서 제시된 첫 번째 해결책입니다.<br /> <br /> 이 글의 예제에서라면 다음과 같이 ConfigureAwait을 호출해주면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public static async Task<string> GetHtmlTextAsync() { var client = new TestClass(); string result = await client.GetTextAsync()<span style='color: blue; font-weight: bold'>.ConfigureAwait(false);</span> return result; } </pre> <br /> <a target='tab' href='http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html'>Don't Block on Async Code</a> 글에서 제시된 두 번째 해결책을 볼까요? 실제로 제 경험으로 비춰보면 GetHtmlTextAsync 메서드에서 Task를 반환하도록 했기 때문에 그에 대해서도 await을 지정하는 것이 더 일반적이었습니다. 그래서 개인적으로는 이 방법을 더 선호합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private <span style='color: blue; font-weight: bold'>async</span> void Form1_Load(object sender, EventArgs e) { var textTask = <span style='color: blue; font-weight: bold'>await</span> GetHtmlTextAsync(); this.Text = textTask; } public static async Task<string> GetHtmlTextAsync() { var client = new TestClass(); string result = await client.GetTextAsync(); return result; } </pre> <br /> 제가 두 번째 방법을 선호하는 또 다른 이유가 있는데, 첫 번째 방법이 가끔 여전히 hang을 발생시키기 때문입니다. 가령 WebClient의 경우 ConfigureAwait(false) 처리를 해도 hang 현상이 발생하는 것은 마찬가지입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Net; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { Uri uri = new Uri("http://www.sysnet.pe.kr"); var textTask = GetHtmlTextAsync(uri); this.Text = textTask.Result; // 여전히 hang 현상은 발생함. } public static async Task<string> GetHtmlTextAsync(Uri uri) { var client = new WebClient(); { string result = await client.DownloadStringTaskAsync(uri)<span style='color: blue; font-weight: bold'>.ConfigureAwait(false);</span> // 처리를 했지만, return result; } } } } </pre> <br /> 위의 코드를 실행하면 ConfigureAwait(false) 처리를 했음에도 불구하고 여전히 hang이 걸립니다. <strike>이것이 버그인가... 하는 정확한 원인은 저도 아직 잘 모르겠습니다. ^^</strike> (업데이트: <a href='https://www.sysnet.pe.kr/2/0/10801#hang'>두 번째 글에서 이유를 설명</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 위의 코드를 테스트한 예제를 첨부했고 각각의 프로젝트에 포함된 코드는 각각의 현상 별로 나눠놓은 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > WindowsFormsApplication3: 원래의 hang 현상을 발생시키는 코드 WindowsFormsApplication2: 스레드로 재현한 코드 WindowsFormsApplication1: WebClient를 사용한 hang 현상을 재현 WindowsFormsApplication4: (테스트 삼아 만들어 본 TaskCompletionSource 사용 예제) </pre></p> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
9555
(왼쪽의 숫자를 입력해야 합니다.)