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]

... 31  32  33  34  35  36  37  38  39  40  [41]  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12602정성태4/16/20219486VS.NET IDE: 161. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 [1]
12601정성태4/15/20218566.NET Framework: 1040. C# - REST API 대신 github 클라이언트 라이브러리를 통해 프로그래밍으로 접근
12600정성태4/15/20218756.NET Framework: 1039. C# - Kubeconfig의 token 설정 및 인증서 구성을 자동화하는 프로그램
12599정성태4/14/20219429.NET Framework: 1038. C# - 인증서 및 키 파일로부터 pfx/p12 파일을 생성하는 방법파일 다운로드1
12598정성태4/14/20219583.NET Framework: 1037. openssl의 PEM 개인키 파일을 .NET RSACryptoServiceProvider에서 사용하는 방법 (2)파일 다운로드1
12597정성태4/13/20219642개발 환경 구성: 569. csproj의 내용을 공통 설정할 수 있는 Directory.Build.targets / Directory.Build.props 파일
12596정성태4/12/20219388개발 환경 구성: 568. Windows의 80 포트 점유를 해제하는 방법
12595정성태4/12/20218820.NET Framework: 1036. SQL 서버 - varbinary 타입에 대한 문자열의 CAST, CONVERT 변환을 C# 코드로 구현
12594정성태4/11/20218264.NET Framework: 1035. C# - kubectl 명령어 또는 REST API 대신 Kubernetes 클라이언트 라이브러리를 통해 프로그래밍으로 접근 [1]파일 다운로드1
12593정성태4/10/20219403개발 환경 구성: 567. Docker Desktop for Windows - kubectl proxy 없이 k8s 대시보드 접근 방법
12592정성태4/10/20219216개발 환경 구성: 566. Docker Desktop for Windows - k8s dashboard의 Kubeconfig 로그인 및 Skip 방법
12591정성태4/9/202112466.NET Framework: 1034. C# - byte 배열을 Hex(16진수) 문자열로 고속 변환하는 방법 [2]파일 다운로드1
12590정성태4/9/20218976.NET Framework: 1033. C# - .NET 4.0 이하에서 Console.IsInputRedirected 구현 [1]
12589정성태4/8/202110312.NET Framework: 1032. C# - Environment.OSVersion의 문제점 및 윈도우 운영체제의 버전을 구하는 다양한 방법 [1]
12588정성태4/7/202110894개발 환경 구성: 565. PowerShell - New-SelfSignedCertificate를 사용해 CA 인증서 생성 및 인증서 서명 방법
12587정성태4/6/202111662개발 환경 구성: 564. Windows 10 - ClickOnce 배포처럼 사용할 수 있는 MSIX 설치 파일 [1]
12586정성태4/5/20219350오류 유형: 710. Windows - Restart-Computer / shutdown 명령어 수행 시 Access is denied(E_ACCESSDENIED)
12585정성태4/5/20219097개발 환경 구성: 563. 기본 생성된 kubeconfig 파일의 내용을 새롭게 생성한 인증서로 구성하는 방법
12584정성태4/1/20219801개발 환경 구성: 562. kubeconfig 파일 없이 kubectl 옵션만으로 실행하는 방법
12583정성태3/29/202111328개발 환경 구성: 561. kubectl 수행 시 다른 k8s 클러스터로 접속하는 방법
12582정성태3/29/202110009오류 유형: 709. Visual C++ - 컴파일 에러 error C2059: syntax error: '__stdcall'
12581정성태3/28/20219955.NET Framework: 1031. WinForm/WPF에서 Console 창을 띄워 출력하는 방법 (2) - Output 디버깅 출력을 AllocConsole로 우회 [2]
12580정성태3/28/20218689오류 유형: 708. SQL Server Management Studio - Execution Timeout Expired.
12579정성태3/28/20218746오류 유형: 707. 중첩 가상화(Nested Virtualization) - The virtual machine could not be started because this platform does not support nested virtualization.
12578정성태3/27/20219051개발 환경 구성: 560. Docker Desktop for Windows 기반의 Kubernetes 구성 (2) - WSL 2 인스턴스에 kind가 구성한 k8s 서비스 위치
12577정성태3/26/202111128개발 환경 구성: 559. Docker Desktop for Windows 기반의 Kubernetes 구성 - WSL 2 인스턴스에 kind 도구로 k8s 클러스터 구성
... 31  32  33  34  35  36  37  38  39  40  [41]  42  43  44  45  ...