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]

... 91  92  93  94  95  96  97  98  99  100  101  102  103  104  [105]  ...
NoWriterDateCnt.TitleFile(s)
11300정성태9/10/201721131.NET Framework: 681. dotnet.exe - run, exec, build, restore, publish 차이점 [3]
11299정성태9/9/201719785개발 환경 구성: 330. Hyper-V VM의 Internal Network를 Private 유형으로 만드는 방법
11298정성태9/8/201723130VC++: 119. EnumProcesses / EnumProcessModules API 사용 시 주의점 [1]
11297정성태9/8/201719783디버깅 기술: 96. windbg - 풀 덤프에 포함된 모든 닷넷 모듈을 파일로 저장하는 방법
11296정성태9/8/201722924웹: 36. Edge - "이 웹 사이트는 이전 기술에서 실행되며 Internet Explorer에서만 작동합니다." 끄는 방법
11295정성태9/7/201720411디버깅 기술: 95. Windbg - .foreach 사용법
11294정성태9/4/201720075개발 환경 구성: 329. 마이크로소프트의 CoreCLR 프로파일러 예제 빌드 방법 [1]
11293정성태9/4/201720660개발 환경 구성: 328. Visual Studio(devenv.exe)를 배치 파일(.bat)을 통해 실행하는 방법
11292정성태9/4/201718880오류 유형: 419. Cannot connect to WMI provider - Invalid class [0x80041010]
11291정성태9/3/201720708개발 환경 구성: 327. 아파치 서버 2.4를 위한 mod_aspdotnet 마이그레이션
11290정성태9/3/201723953개발 환경 구성: 326. 아파치 서버에서 ASP.NET을 실행하는 mod_aspdotnet 모듈 [2]
11289정성태9/3/201721619개발 환경 구성: 325. GAC에 어셈블리 등록을 위해 gacutil.exe을 사용하는 경우 주의 사항
11288정성태9/3/201718389개발 환경 구성: 324. 윈도우용 XAMPP의 아파치 서버 구성 방법
11287정성태9/1/201727589.NET Framework: 680. C# - 작업자(Worker) 스레드와 UI 스레드 [11]
11286정성태8/28/201714957기타: 67. App Privacy Policy
11285정성태8/28/201723503.NET Framework: 679. C# - 개인 키 보안의 SFTP를 이용한 파일 업로드파일 다운로드1
11284정성태8/27/201721530.NET Framework: 678. 데스크톱 윈도우 응용 프로그램에서 UWP 라이브러리를 이용한 비디오 장치 열람하는 방법 [1]파일 다운로드1
11283정성태8/27/201717305오류 유형: 418. CSS3117: @font-face failed cross-origin request. Resource access is restricted.
11282정성태8/26/201719746Math: 22. 행렬로 바라보는 피보나치 수열
11281정성태8/26/201721559.NET Framework: 677. Visual Studio 2017 - NuGet 패키지를 직접 참조하는 PackageReference 지원 [2]
11280정성태8/24/201718577디버깅 기술: 94. windbg - 풀 덤프에 포함된 모든 모듈을 파일로 저장하는 방법
11279정성태8/23/201730179.NET Framework: 676. C# Thread가 Running 상태인지 아는 방법
11278정성태8/23/201718369오류 유형: 417. TFS - Warning - Unable to refresh ... because you have a pending edit. [1]
11277정성태8/23/201719622오류 유형: 416. msbuild - error MSB4062: The "TransformXml" task could not be loaded from the assembly
11276정성태8/23/201723917.NET Framework: 675. C# - (파일) 확장자와 연결된 실행 파일 경로 찾기 [2]파일 다운로드1
11275정성태8/23/201732920개발 환경 구성: 323. Visual Studio 설치 없이 빌드 환경 구성 - Visual Studio 2017용 Build Tools [1]
... 91  92  93  94  95  96  97  98  99  100  101  102  103  104  [105]  ...