성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] What has case distinction but is ne...
[정성태] 대소문자 '변환'과 함께 따라오는 문제가 바로 대소문자 구분 없...
[정성태] Reverse-engineering what a "short" ...
[정성태] 윈도우의 경우, 스레드 관련 자원을 완전히 회수하기 위해 Thr...
[지현명] Android쪽에서 activity 접근 할때 아래꺼 적어 놓고...
[지현명] Maui.Android에서 폴더 관련 내용 정리 잘 되어 있네요...
[정성태] @정한솔 언급하신 사항이 맞습니다. (C# 13부터) 중간에 i...
[정한솔] 기존 printAll(params int[] args)를 pri...
[정성태] 제가 거기까지는 테스트를 안 해봤군요. ^^ 재미 삼아 저 당시...
[정재겸] 질문이 하나 더 있습니다. 처음에 키보드 인풋은 먹다가 마...
글쓰기
제목
이름
암호
전자우편
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'>WebClient 타입의 ...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;' > async/await 사용 시 hang 문제가 발생하는 경우 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1541'>http://www.sysnet.pe.kr/2/0/1541</a> </pre> <br /> WebClient의 비동기 메서드 호출 시 hang 현상이 발생하는 경우를 다뤘었습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // Windows Forms 응용 프로그램 private void Form1_Load(object sender, EventArgs e) { Uri uri = new Uri("http://www.sysnet.pe.kr"); Task<string> textTask = GetHtmlTextAsync(uri); this.Text = <span style='color: blue; font-weight: bold'>textTask.Result</span>; // hang 현상 발생함. } public static async Task<string> GetHtmlTextAsync(Uri uri) { var client = new WebClient(); { string result = <span style='color: blue; font-weight: bold'>await client.DownloadStringTaskAsync(uri)</span>; return result; } } </pre> <br /> 전에 설명한 대로 위의 현상은 SynchronizationContext의 영향이기 때문에 이해가 갑니다. 하지만, await 구문에서 다음과 같이 ConfigureAwait(false) 처리를 해도 마찬가지 결과가 나온다는 것은 좀 이상합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > string result = await client.DownloadStringTaskAsync(uri).<span style='color: blue; font-weight: bold'>ConfigureAwait(false);</span> </pre> <br /> 분명히 ConfigureAwait(false) 호출은 SynchronizationContext와는 상관이 없는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 비동기 메서드 내에서 await 시 ConfigureAwait 호출 의미 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11418'>http://www.sysnet.pe.kr/2/0/11418</a> </pre> <br /> Task.Result 호출에서 스레드가 dead-lock 걸리는 것이 말이 안 됩니다. 도대체 원인이 뭘까요? ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 원인을 밝히기 위해 DownloadStringTaskAsync를 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [ComVisible(false), HostProtection(SecurityAction.LinkDemand, ExternalThreading=true)] public Task<string> DownloadStringTaskAsync(Uri address) { <>c__DisplayClass219_0 class_ = new <>c__DisplayClass219_0(); class_.<>4__this = this; <span style='color: blue; font-weight: bold'>class_.tcs = new TaskCompletionSource<string>(address);</span> class_.handler = null; class_.handler = new DownloadStringCompletedEventHandler(class_.<DownloadStringTaskAsync>b__0); this.DownloadStringCompleted += class_.handler; try { <span style='color: blue; font-weight: bold'>this.DownloadStringAsync(address, class_.tcs);</span> } catch { this.DownloadStringCompleted -= class_.handler; throw; } return class_.tcs.Task; } </pre> <br /> Task 객체를 담은 TaskCompletionSource를 생성하는 것외에는 딱히 별다른 것 없이 DownloadStringAsync를 호출합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [HostProtection(SecurityAction.LinkDemand, ExternalThreading=true)] public void DownloadStringAsync(Uri address, object userToken) { if (Logging.On) { Logging.Enter(Logging.Web, this, "DownloadStringAsync", address); } if (address == null) { throw new ArgumentNullException("address"); } this.InitWebClientAsync(); this.ClearWebClientState(); <span style='color: blue; font-weight: bold'>AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(userToken);</span> this.m_AsyncOp = asyncOp; try { WebRequest request = this.m_WebRequest = this.GetWebRequest(this.GetUri(address)); <span style='color: blue; font-weight: bold'>this.DownloadBits</span>(request, null, new CompletionDelegate(this.DownloadStringAsyncCallback), <span style='color: blue; font-weight: bold'>asyncOp</span>); } catch (Exception exception) { if (((exception is ThreadAbortException) || (exception is StackOverflowException)) || (exception is OutOfMemoryException)) { throw; } if (!(exception is WebException) && !(exception is SecurityException)) { exception = new WebException(SR.GetString("net_webclient"), exception); } this.DownloadStringAsyncCallback(null, exception, asyncOp); } if (Logging.On) { Logging.Exit(Logging.Web, this, "DownloadStringAsync", ""); } } </pre> <br /> 위의 코드에서 DownloadStringAsync 메서드 역시 별다르게 하는 일은 없고 대부분의 동작을 DownloadBits 메서드에 위임합니다. 하지만, 특이하게 AsyncOperationManager.CreateOperation으로 비동기 처리를 위한 옵션을 구성하는데요, 바로 여기에 답이 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [__DynamicallyInvokable] public static AsyncOperation CreateOperation(object userSuppliedState) { return <span style='color: blue; font-weight: bold'>AsyncOperation.CreateOperation</span>(userSuppliedState, <span style='color: blue; font-weight: bold'>SynchronizationContext</span>); } [EditorBrowsable(EditorBrowsableState.Advanced), __DynamicallyInvokable] public static SynchronizationContext SynchronizationContext { [__DynamicallyInvokable] get { if (SynchronizationContext.Current == null) { SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); } <span style='color: blue; font-weight: bold'>return SynchronizationContext.Current; // Windows Forms의 SynchronizationContext 문맥 반환 </span> } [__DynamicallyInvokable, PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")] set { SynchronizationContext.SetSynchronizationContext(value); } } </pre> <br /> 보는 바와 같이 SynchronizationContext.Current로 비동기를 구성하고 있기 때문에 애당초 DownloadStringTaskAsync 메서드는 ConfigureAwait(false)에 상관없이 SynchronizationContext에 기반을 둬 비동기 처리를 하고 있었던 것입니다. 확인을 위해 DownloadBits에 전달된,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > this.DownloadBits(request, null, new CompletionDelegate(<span style='color: blue; font-weight: bold'>this.DownloadStringAsyncCallback</span>), asyncOp); </pre> <br /> 완료 callback 메서드인 DownloadStringAsyncCallback를 따라가 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > private void DownloadStringAsyncCallback(byte[] returnBytes, Exception exception, object state) { <span style='color: blue; font-weight: bold'>AsyncOperation asyncOp = (AsyncOperation) state;</span> string result = null; try { if (returnBytes != null) { result = this.GetStringUsingEncoding(this.m_WebRequest, returnBytes); } } catch (Exception exception2) { if (((exception2 is ThreadAbortException) || (exception2 is StackOverflowException)) || (exception2 is OutOfMemoryException)) { throw; } exception = exception2; } DownloadStringCompletedEventArgs eventArgs = new DownloadStringCompletedEventArgs(result, exception, this.m_Cancelled, asyncOp.UserSuppliedState); <span style='color: blue; font-weight: bold'>this.InvokeOperationCompleted</span>(asyncOp, this.downloadStringOperationCompleted, eventArgs); } private void InvokeOperationCompleted(AsyncOperation asyncOp, SendOrPostCallback callback, AsyncCompletedEventArgs eventArgs) { if (Interlocked.CompareExchange<AsyncOperation>(ref this.m_AsyncOp, null, asyncOp) == asyncOp) { this.CompleteWebClientState(); <span style='color: blue; font-weight: bold'>asyncOp.PostOperationCompleted</span>(callback, eventArgs); } } [__DynamicallyInvokable] public void PostOperationCompleted(SendOrPostCallback d, object arg) { <span style='color: blue; font-weight: bold'>this.Post(d, arg);</span> this.OperationCompletedCore(); } [__DynamicallyInvokable] public void Post(SendOrPostCallback d, object arg) { this.VerifyNotCompleted(); this.VerifyDelegateNotNull(d); <span style='color: blue; font-weight: bold'>this.syncContext.Post(d, arg);</span> } </pre> <br /> 결국 콜백 메서드를 <a target='tab' href='http://www.sysnet.pe.kr/2/0/10801'>SynchronizationContext.Post</a>를 이용해 호출하고 있습니다. 정리해 보면, DownloadStringTaskAsync뿐만 아니라 내부적으로 AsyncOperationManager.CreateOperation을 사용한 모든 ...Async 메서드들은 ConfigureAwait(true)를 한 것과 동일한 비동기 처리를 합니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 참고로 다음과 같이 간단하게(?) DownloadStringTaskAsync와 유사한 재현 코드를 만드는 것도 가능합니다.<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.ComponentModel; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { Task<string> textTask = TestAsync(); this.Text = textTask.Result; // hang } public delegate string DummyMethodDelegate(); public class DummyAsyncState { public AsyncOperation AsyncOp; public SendOrPostCallback TaskCompleted; public string Result; } public static async Task<string> TestAsync() { TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(null); AsyncOperation asyncOp = null; asyncOp = AsyncOperationManager.CreateOperation(tcs); var state = new DummyAsyncState { AsyncOp = asyncOp, TaskCompleted = actionDummyCompleted }; DummyMethodDelegate dummy = new DummyMethodDelegate(dummyEndFunc); var tuple = new Tuple<DummyMethodDelegate, DummyAsyncState>(dummy, state); dummy.BeginInvoke(calcCompleted, tuple); return await tcs.Task.<span style='color: blue; font-weight: bold'>ConfigureAwait(false)</span>; } public static void actionDummyCompleted(object arg) { DummyAsyncState state = arg as DummyAsyncState; var tcs = state.AsyncOp.UserSuppliedState as TaskCompletionSource<string>; tcs.TrySetResult(state.Result); } public static string dummyEndFunc() { return "TEST"; } static void calcCompleted(IAsyncResult ar) { var tuple = ar.AsyncState as Tuple<DummyMethodDelegate, DummyAsyncState>; if (ar.IsCompleted == true) { string result = tuple.Item1.EndInvoke(ar); var state = tuple.Item2 as DummyAsyncState; state.Result = result; <span style='color: blue; font-weight: bold'>state.AsyncOp.PostOperationCompleted(state.TaskCompleted, state);</span> } } } } </pre> <br /> 위의 코드는 ConfigureAwait(false)을 했지만, AsyncOperation에 의해 콜백 메서드가 SynchronizationContext 상에서 실행되었기 때문에 hang 현상이 발생하는 것입니다. 만약에 다음과 같이 AsyncOperation을 사용하지 않고 ThreadPool을 이용해 구현했다면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // state.AsyncOp.PostOperationCompleted(state.TaskCompleted, state); System.Threading.ThreadPool.QueueUserWorkItem(state.TaskCompleted, state); </pre> <br /> ConfigureAwait(false) 옵션이 적용되어 hang 현상에 빠지지 않습니다. 물론 ConfigureAwait(true)로 하면 hang 현상에 빠지지만 이것은 ThreadPool.QueueUserWorkItem으로 처리한 것과 상관없이 await의 문제입니다.<br /> <br /> (<a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1208&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 정리해 보면, 여러분이 사용할 비동기 메서드에서 내부적으로 AsyncOperation을 사용하고 있는지 (이번 글에서처럼 분석하기 전에는) 외부에서 알 방법이 없습니다.<br /> <br /> 따라서 다음의 글에서 했던 2번째 조언이,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Async/Await - Best Practices in Asynchronous Programming ; <a target='tab' href='https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming'>https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming</a> </pre> <br /> 가장 적절한 해결책입니다. "Async all the way"!<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1807
(왼쪽의 숫자를 입력해야 합니다.)