Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)
(시리즈 글이 7개 있습니다.)
.NET Framework: 394. async/await 사용 시 hang 문제가 발생하는 경우
; https://www.sysnet.pe.kr/2/0/1541

.NET Framework: 512. async/await 사용 시 hang 문제가 발생하는 경우 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/10801

.NET Framework: 631. async/await에 대한 "There Is No Thread" 글의 부가 설명
; https://www.sysnet.pe.kr/2/0/11129

.NET Framework: 720. 비동기 메서드 내에서 await 시 ConfigureAwait 호출 의미
; https://www.sysnet.pe.kr/2/0/11418

.NET Framework: 721. WebClient 타입의 ...Async 메서드 호출은 왜 await + 동기 호출 시 hang 현상이 발생할까요?
; https://www.sysnet.pe.kr/2/0/11419

디버깅 기술: 196. windbg - async/await 비동기인 경우 메모리 덤프 분석의 어려움
; https://www.sysnet.pe.kr/2/0/13563

닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
; https://www.sysnet.pe.kr/2/0/13573




WebClient 타입의 ...Async 메서드 호출은 왜 await + 동기 호출 시 hang 현상이 발생할까요?

예전에 쓴 글에서,

async/await 사용 시 hang 문제가 발생하는 경우
; https://www.sysnet.pe.kr/2/0/1541

WebClient의 비동기 메서드 호출 시 hang 현상이 발생하는 경우를 다뤘었습니다.

// Windows Forms 응용 프로그램

private void Form1_Load(object sender, EventArgs e)
{
    Uri uri = new Uri("https://www.sysnet.pe.kr");

    Task<string> 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);
        return result;
    }
}

전에 설명한 대로 위의 현상은 SynchronizationContext의 영향이기 때문에 이해가 갑니다. 하지만, await 구문에서 다음과 같이 ConfigureAwait(false) 처리를 해도 마찬가지 결과가 나온다는 것은 좀 이상합니다.

string result = await client.DownloadStringTaskAsync(uri).ConfigureAwait(false);

분명히 ConfigureAwait(false) 호출은 SynchronizationContext와는 상관이 없는데,

비동기 메서드 내에서 await 시 ConfigureAwait 호출 의미
; https://www.sysnet.pe.kr/2/0/11418

Task.Result 호출에서 스레드가 dead-lock 걸리는 것이 말이 안 됩니다. 도대체 원인이 뭘까요? ^^




원인을 밝히기 위해 DownloadStringTaskAsync를 보겠습니다.

[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;
    class_.tcs = new TaskCompletionSource<string>(address);
    class_.handler = null;
    class_.handler = new DownloadStringCompletedEventHandler(class_.<DownloadStringTaskAsync>b__0);
    this.DownloadStringCompleted += class_.handler;
    try
    {
        this.DownloadStringAsync(address, class_.tcs);
    }
    catch
    {
        this.DownloadStringCompleted -= class_.handler;
        throw;
    }
    return class_.tcs.Task;
}

Task 객체를 담은 TaskCompletionSource를 생성하는 것외에는 딱히 별다른 것 없이 DownloadStringAsync를 호출합니다.

[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();
    AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(userToken);
    this.m_AsyncOp = asyncOp;
    try
    {
        WebRequest request = this.m_WebRequest = this.GetWebRequest(this.GetUri(address));
        this.DownloadBits(request, null, new CompletionDelegate(this.DownloadStringAsyncCallback), asyncOp);
    }
    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", "");
    }
}

위의 코드에서 DownloadStringAsync 메서드 역시 별다르게 하는 일은 없고 대부분의 동작을 DownloadBits 메서드에 위임합니다. 하지만, 특이하게 AsyncOperationManager.CreateOperation으로 비동기 처리를 위한 옵션을 구성하는데요, 바로 여기에 답이 있습니다.

[__DynamicallyInvokable]
public static AsyncOperation CreateOperation(object userSuppliedState)
{
    return AsyncOperation.CreateOperation(userSuppliedState, SynchronizationContext);
}

[EditorBrowsable(EditorBrowsableState.Advanced), __DynamicallyInvokable]
public static SynchronizationContext SynchronizationContext
{
    [__DynamicallyInvokable]
    get
    {
        if (SynchronizationContext.Current == null)
        {
            SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
        }
        return SynchronizationContext.Current; // Windows Forms의 SynchronizationContext 문맥 반환  
    }
    [__DynamicallyInvokable, PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")]
    set
    {
        SynchronizationContext.SetSynchronizationContext(value);
    }
}

보는 바와 같이 SynchronizationContext.Current로 비동기를 구성하고 있기 때문에 애당초 DownloadStringTaskAsync 메서드는 ConfigureAwait(false)에 상관없이 SynchronizationContext에 기반을 둬 비동기 처리를 하고 있었던 것입니다. 확인을 위해 DownloadBits에 전달된,

this.DownloadBits(request, null, new CompletionDelegate(this.DownloadStringAsyncCallback), asyncOp);

완료 callback 메서드인 DownloadStringAsyncCallback를 따라가 보면,

private void DownloadStringAsyncCallback(byte[] returnBytes, Exception exception, object state)
{
    AsyncOperation asyncOp = (AsyncOperation) state;
    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);
    this.InvokeOperationCompleted(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();
        asyncOp.PostOperationCompleted(callback, eventArgs);
    }
}

[__DynamicallyInvokable]
public void PostOperationCompleted(SendOrPostCallback d, object arg)
{
    this.Post(d, arg);
    this.OperationCompletedCore();
}

[__DynamicallyInvokable]
public void Post(SendOrPostCallback d, object arg)
{
    this.VerifyNotCompleted();
    this.VerifyDelegateNotNull(d);
    this.syncContext.Post(d, arg);
}

결국 콜백 메서드를 SynchronizationContext.Post를 이용해 호출하고 있습니다. 정리해 보면, DownloadStringTaskAsync뿐만 아니라 내부적으로 AsyncOperationManager.CreateOperation을 사용한 모든 ...Async 메서드들은 ConfigureAwait(true)를 한 것과 동일한 비동기 처리를 합니다.




참고로 다음과 같이 간단하게(?) DownloadStringTaskAsync와 유사한 재현 코드를 만드는 것도 가능합니다.

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.ConfigureAwait(false);
        }

        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;

                state.AsyncOp.PostOperationCompleted(state.TaskCompleted, state);
            }
        }
    }
}

위의 코드는 ConfigureAwait(false)을 했지만, AsyncOperation에 의해 콜백 메서드가 SynchronizationContext 상에서 실행되었기 때문에 hang 현상이 발생하는 것입니다. 만약에 다음과 같이 AsyncOperation을 사용하지 않고 ThreadPool을 이용해 구현했다면,

// state.AsyncOp.PostOperationCompleted(state.TaskCompleted, state);

System.Threading.ThreadPool.QueueUserWorkItem(state.TaskCompleted, state);

ConfigureAwait(false) 옵션이 적용되어 hang 현상에 빠지지 않습니다. 물론 ConfigureAwait(true)로 하면 hang 현상에 빠지지만 이것은 ThreadPool.QueueUserWorkItem으로 처리한 것과 상관없이 await의 문제입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




정리해 보면, 여러분이 사용할 비동기 메서드에서 내부적으로 AsyncOperation을 사용하고 있는지 (이번 글에서처럼 분석하기 전에는) 외부에서 알 방법이 없습니다.

따라서 다음의 글에서 했던 2번째 조언이,

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

가장 적절한 해결책입니다. "Async all the way"!




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/7/2022]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2017-12-23 02시19분
[이성환] 이거였군요!!!

결국 ConfigureAwait() 역시 대상 Task 를 반환 받은 후 호출되는 것이니 당연히 대상 메서드의 구현을 먼저 봤어야 하는 건데,
애먼 ConfigureAwait만 뜯어보다 원인 찾기를 포기했었던 기억이 나네요..=ㅅ=;;
AsyncOperation 은 SynchronizationContext 를 편리하게 사용하기 위해 추가된 녀석인데, 특히 EAP 구현에 많이 사용됩니다.
대표적으로는 UI 비동기 처리를 도와주는 BackgroundWorker 가 AsyncOperation 을 이용해 비동기 처리를 하고 있죠.
그리고 본문을 보니 DownloadStringTaskAsync(Uri address) 역시 EAP 로 기본 구현이 되어 있는 것을 awaitable 할 수 있도록 랩핑한 것이네요.
이제야 BackgroundWorker 와 Task 를 섞어 쓰다 hang 이 발생한 이유를 알았습니다..;;
당시에는 원인을 못 찾아 결국 스레드풀 처리를 했었는데....;ㅂ;

여기서 개인적으로 느끼는 문제 중 하나는 네이밍 입니다.
EAP 로 구현되어 제공되는 BCL의 메서드들 네이밍이 저렇게 Async 접미로 끝나는데요. TAP의 네이밍 룰도 Async 접미이다 보니 이게 TAP 와 겹치는 경우가 많습니다.
물론 시그니처와 반환 타입이 달라서 구분은 되지만, 나중에 나온 TAP 는 다른 방식의 네이밍 룰을 만들었어야 하지 않았나 합니다. (ByTask나 AsAsync 따우로...)
새로운 오버로딩을 만들 때에도 문제가 되고, 일관성이 없어서 불편한 점도 꽤 있습니다.
이것도 네이밍 룰에서 차이가 있었다면 한 번쯤 의심해보고 넘어갔을 거 같은데 고정관념에 사로잡혀 생각지도 못했네요. -ㅁ-;;

여튼 또 하나 배우고 갑니다.
멋진 분석 감사합니다.
[guest]
2019-05-28 05시11분
[이동우] 잘보고 잘배우고 갑니당
[guest]

... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13155정성태11/4/20226298디버깅 기술: 183. TCP 동시 접속 (연결이 아닌) 시도를 1개로 제한한 서버
13154정성태11/3/20225775.NET Framework: 2063. .NET 5+부터 지원되는 GC.GetGCMemoryInfo파일 다운로드1
13153정성태11/2/20227054.NET Framework: 2062. C# - 코드로 재현하는 소켓 상태(SYN_SENT, SYN_RECV)
13152정성태11/1/20225677.NET Framework: 2061. ASP.NET Core - DI로 추가한 클래스의 초기화 방법 [1]
13151정성태10/31/20225781C/C++: 161. Windows 11 환경에서 raw socket 테스트하는 방법파일 다운로드1
13150정성태10/30/20225842C/C++: 160. Visual Studio 2022로 빌드한 C++ 프로그램을 위한 다른 PC에서 실행하는 방법
13149정성태10/27/20225739오류 유형: 825. C# - CLR ETW 이벤트 수신이 GCHeapStats_V1/V2에 대해 안 되는 문제파일 다운로드1
13148정성태10/26/20225753오류 유형: 824. msbuild 에러 - error NETSDK1005: Assets file '...\project.assets.json' doesn't have a target for 'net5.0'. Ensure that restore has run and that you have included 'net5.0' in the TargetFramew
13147정성태10/25/20224851오류 유형: 823. Visual Studio 2022 - Unable to attach to CoreCLR. The debugger's protocol is incompatible with the debuggee.
13146정성태10/24/20225689.NET Framework: 2060. C# - Java의 Xmx와 유사한 힙 메모리 최댓값 제어 옵션 HeapHardLimit
13145정성태10/21/20225974오류 유형: 822. db2 - Password validation for user db2inst1 failed with rc = -2146500508
13144정성태10/20/20225801.NET Framework: 2059. ClrMD를 이용해 윈도우 환경의 메모리 덤프로부터 닷넷 모듈을 추출하는 방법파일 다운로드1
13143정성태10/19/20226334오류 유형: 821. windbg/sos - Error code - 0x000021BE
13142정성태10/18/20225156도서: 시작하세요! C# 12 프로그래밍
13141정성태10/17/20226818.NET Framework: 2058. [in,out] 배열을 C#에서 C/C++로 넘기는 방법 - 세 번째 이야기파일 다운로드1
13140정성태10/11/20226178C/C++: 159. C/C++ - 리눅스 환경에서 u16string 문자열을 출력하는 방법 [2]
13139정성태10/9/20226008.NET Framework: 2057. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 모든 닷넷 모듈을 추출하는 방법파일 다운로드1
13138정성태10/8/20227322.NET Framework: 2056. C# - await 비동기 호출을 기대한 메서드가 동기로 호출되었을 때의 부작용 [1]
13137정성태10/8/20225668.NET Framework: 2055. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 닷넷 모듈을 추출하는 방법
13136정성태10/7/20226239.NET Framework: 2054. .NET Core/5+ SDK 설치 없이 dotnet-dump 사용하는 방법
13135정성태10/5/20226486.NET Framework: 2053. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프를 분석하는 방법 - 두 번째 이야기
13134정성태10/4/20225226오류 유형: 820. There is a problem with AMD Radeon RX 5600 XT device. For more information, search for 'graphics device driver error code 31'
13133정성태10/4/20225545Windows: 211. Windows - (commit이 아닌) reserved 메모리 사용량 확인 방법 [1]
13132정성태10/3/20225444스크립트: 42. 파이썬 - latexify-py 패키지 소개 - 함수를 mathjax 식으로 표현
13131정성태10/3/20228161.NET Framework: 2052. C# - Windows Forms의 데이터 바인딩 지원(DataBinding, DataSource) [2]파일 다운로드1
13130정성태9/28/20225195.NET Framework: 2051. .NET Core/5+ - 에러 로깅을 위한 Middleware가 동작하지 않는 경우파일 다운로드1
... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...