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)
12928정성태1/18/20226949개발 환경 구성: 629. AKS/Kubernetes에서 호스팅 중인 pod에 shell(/bin/bash)로 진입하는 방법
12927정성태1/18/20226702개발 환경 구성: 628. AKS 환경에 응용 프로그램 배포 방법
12926정성태1/17/20227187오류 유형: 787. AKS - pod 배포 시 ErrImagePull/ImagePullBackOff 오류
12925정성태1/17/20227280개발 환경 구성: 627. AKS의 준비 단계 - ACR(Azure Container Registry)에 docker 이미지 배포
12924정성태1/15/20228799.NET Framework: 1134. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) [2]파일 다운로드1
12923정성태1/15/20227688개발 환경 구성: 626. ffmpeg.exe를 사용해 비디오 파일을 MPEG1 포맷으로 변경하는 방법
12922정성태1/14/20226744개발 환경 구성: 625. AKS - Azure Kubernetes Service 생성 및 SLO/SLA 변경 방법
12921정성태1/14/20225718개발 환경 구성: 624. Docker Desktop에서 별도 서버에 설치한 docker registry에 이미지 올리는 방법
12920정성태1/14/20226477오류 유형: 786. Camtasia - An error occurred with the camera: Failed to Add Video Sampler.
12919정성태1/13/20226320Windows: 199. Host Network Service (HNS)에 의해서 점유되는 포트
12918정성태1/13/20226567Linux: 47. WSL - shell script에서 설정한 환경 변수가 스크립트 실행 후 반영되지 않는 문제
12917정성태1/12/20225777오류 유형: 785. C# - The type or namespace name '...' could not be found (are you missing a using directive or an assembly reference?)
12916정성태1/12/20225504오류 유형: 784. TFS - One or more source control bindings for this solution are not valid and are listed below.
12915정성태1/11/20225790오류 유형: 783. Visual Studio - We didn't find any interpreters
12914정성태1/11/20227732VS.NET IDE: 172. 비주얼 스튜디오 2022의 파이선 개발 환경 지원
12913정성태1/11/20228233.NET Framework: 1133. C# - byte * (바이트 포인터)를 FileStream으로 쓰는 방법 [1]
12912정성태1/11/20228879개발 환경 구성: 623. ffmpeg.exe를 사용해 비디오 파일의 이미지를 PGM(Portable Gray Map) 파일 포맷으로 출력하는 방법 [1]
12911정성태1/11/20226197VS.NET IDE: 171. 비주얼 스튜디오 - 더 이상 만들 수 없는 "ASP.NET Core 3.1 Web Application (.NET Framework)" 프로젝트
12910정성태1/10/20226675제니퍼 .NET: 30. 제니퍼 닷넷 적용 사례 (8) - CPU high와 DB 쿼리 성능에 문제가 함께 있는 사이트
12909정성태1/10/20228077오류 유형: 782. Visual Studio 2022 설치 시 "Couldn't install Microsoft.VisualCpp.Redist.14.Latest"
12908정성태1/10/20225929.NET Framework: 1132. C# - ref/out 매개변수의 IL 코드 처리
12907정성태1/9/20226399오류 유형: 781. (youtube-dl.exe) 실행 시 "This app can't run on your PC" / "Access is denied." 오류 발생
12906정성태1/9/20227017.NET Framework: 1131. C# - 네임스페이스까지 동일한 타입을 2개의 DLL에서 제공하는 경우 충돌을 우회하는 방법 [1]파일 다운로드1
12905정성태1/8/20226673오류 유형: 780. Could not load file or assembly 'Microsoft.VisualStudio.TextTemplating.VSHost.15.0, Version=16.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies.
12904정성태1/8/20228693개발 환경 구성: 623. Visual Studio 2022 빌드 환경을 위한 github Actions 설정 [1]
12903정성태1/7/20227281.NET Framework: 1130. C# - ELEMENT_TYPE_INTERNAL 유형의 사용 예
... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...