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]

1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13402정성태8/20/20233326닷넷: 2137. ILSpy의 nuget 라이브러리 버전 - ICSharpCode.Decompiler
13401정성태8/19/20233562닷넷: 2136. .NET 5+ 환경에서 P/Invoke의 성능을 높이기 위한 SuppressGCTransition 특성 [1]
13400정성태8/10/20233427오류 유형: 874. 파이썬 - pymssql을 윈도우 환경에서 설치 불가
13399정성태8/9/20233443닷넷: 2135. C# - 지역 변수로 이해하는 메서드 매개변수의 값/참조 전달
13398정성태8/3/20234223스크립트: 55. 파이썬 - pyodbc를 이용한 SQL Server 연결 사용법
13397정성태7/23/20233726닷넷: 2134. C# - 문자열 연결 시 string.Create를 이용한 GC 할당 최소화
13396정성태7/22/20233428스크립트: 54. 파이썬 pystack 소개 - 메모리 덤프로부터 콜 스택 열거
13395정성태7/20/20233347개발 환경 구성: 685. 로컬에서 개발 중인 ASP.NET Core/5+ 웹 사이트에 대해 localhost 이외의 호스트 이름으로 접근하는 방법
13394정성태7/16/20233317오류 유형: 873. Oracle.ManagedDataAccess.Client - 쿼리 수행 시 System.InvalidOperationException
13393정성태7/16/20233482닷넷: 2133. C# - Oracle 데이터베이스의 Sleep 쿼리 실행하는 방법
13392정성태7/16/20233354오류 유형: 872. Oracle - ORA-01031: insufficient privileges
13391정성태7/14/20233431닷넷: 2132. C# - sealed 클래스의 메서드를 callback 호출했을 때 인라인 처리가 될까요?
13390정성태7/12/20233390스크립트: 53. 파이썬 - localhost 호출 시의 hang 현상
13389정성태7/5/20233427개발 환경 구성: 684. IIS Express로 호스팅하는 웹을 WSL 환경에서 접근하는 방법
13388정성태7/3/20233566오류 유형: 871. 윈도우 탐색기에서 열리지 않는 zip 파일 - The Compressed (zipped) Folder '[...].zip' is invalid. [1]파일 다운로드1
13387정성태6/28/20233581오류 유형: 870. _mysql - Commands out of sync; you can't run this command now
13386정성태6/27/20233661Linux: 61. docker - 원격 제어를 위한 TCP 바인딩 추가
13385정성태6/27/20233878Linux: 60. Linux - 외부에서의 접속을 허용하기 위한 TCP 포트 여는 방법
13384정성태6/26/20233621.NET Framework: 2131. C# - Source Generator로 해결하는 enum 박싱 문제파일 다운로드1
13383정성태6/26/20233373개발 환경 구성: 683. GPU 런타임을 사용하는 Colab 노트북 설정
13382정성태6/25/20233412.NET Framework: 2130. C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)파일 다운로드1
13381정성태6/25/20233798오류 유형: 869. Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
13380정성태6/24/20233242스크립트: 52. 파이썬 3.x에서의 동적 함수 추가
13379정성태6/23/20233252스크립트: 51. 파이썬 2.x에서의 동적 함수 추가
13378정성태6/22/20233169오류 유형: 868. docker - build 시 "CANCELED ..." 뜨는 문제
13377정성태6/22/20236984오류 유형: 867. 파이썬 mysqlclient 2.2.x 설치 시 "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" 오류
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...