Microsoft MVP성태의 닷넷 이야기
닷넷: 2223. C# - await 호출과 WPF의 Dispatcher Queue 동작 확인 [링크 복사], [링크+제목 복사],
조회: 1933
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

(시리즈 글이 3개 있습니다.)
닷넷: 2222. C# - WPF의 Dispatcher Queue 동작 확인
; https://www.sysnet.pe.kr/2/0/13570

닷넷: 2223. C# - await 호출과 WPF의 Dispatcher Queue 동작 확인
; https://www.sysnet.pe.kr/2/0/13571

닷넷: 2224. C# - WPF의 Dispatcher Queue로 알아보는 await 호출의 hang 현상
; https://www.sysnet.pe.kr/2/0/13572




C# - await 호출과 WPF의 Dispatcher Queue 동작 확인

지난 글에서,

C# - WPF의 Dispatcher Queue 동작 확인
; https://www.sysnet.pe.kr/2/0/13570

Dispatcher에 대한 Invoke... 유의 메서드 실행에 따른 동작을 확인했습니다. 이번 글에서는, await 상황에서 어떻게 동작하는지 좀 더 설명을 해볼 텐데요, 이를 위해 예제 코드에 await 호출을 추가한 다음,

int _callCount = 0;

private async void Window_Loaded(object sender, RoutedEventArgs e)
{
    System.Diagnostics.Trace.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] {_callCount} =====Window_Loaded step1=====");
    await Task.Delay(16);
    System.Diagnostics.Trace.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] {_callCount} =====Window_Loaded step2=====");
            _callCount++;
}

private void Hooks_OperationPosted(object sender, System.Windows.Threading.DispatcherHookEventArgs e)
{
    string name = GetName(e.Operation);
    Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] {name}");
}

실행해 보면 이런 결과가 나옵니다.

...[생략]...
[1] System.Windows.Input.CommandManager.RaiseRequerySuggested
[1] 0 =====Window_Loaded step1=====
[1] System.Windows.Window.<PostContentRendered>b__204_0
[3] System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation+<>c.<.cctor>b__8_0
[1] 0 =====Window_Loaded step2=====
[1] System.Windows.Media.MediaContext.RenderMessageHandler
...[생략]...

위의 결과를 해석해 볼까요? ^^ 우선, "await Task.Delay(16)"로 인해 비동기 호출이 되었고, 약 16ms 이후에 스레드풀의 여유 스레드가 callback을 받게 됩니다. 바로 그 스레드가 3번 스레드인 것입니다.

3번 스레드는 await 이후의 코드를 담은 (C# 컴파일러가 IAsyncStateMachine을 상속받아 생성한 타입의) 메서드를 직접 호출하지는 않고, 단순히 Dispatcher에 전달하는 역할만 합니다. 그래서 (await 이후의 코드에 해당하는) "Window_Loaded step2" 코드는 UI 스레드에서 실행되므로 "[1]"번 스레드 값을 출력하는 것입니다.




자, 그럼 이것도 우리가 직접 호출을 해볼까요? 재미있게도, 이번에는 지난번처럼 _method를 가져오면 Action 타입이 아닌, SendOrPostCallback 타입으로 나옵니다.

private void Hooks_OperationPosted(object sender, System.Windows.Threading.DispatcherHookEventArgs e)
{
    string name = GetName(e.Operation);
    Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] {name}");

    string target = "System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation+<>c.<.cctor>b__8_0";
    if (name == target)
    {
        Console.WriteLine($"============ {e.Operation.Task.Id} ==============");
                
        System.Delegate method = GetMethod(e.Operation);

        if (method is Action action)
        {
            action();
        }
        else if (method is SendOrPostCallback callback)
        {
            callback(null);
        }
    }
}

일단 callback의 인자로 null을 넣었는데요, 실행해 보면 "System.NullReferenceException: 'Object reference not set to an instance of an object.'" 예외가 발생합니다. 오호~~~ 뭔가 전달해야 한다는 건데요, 그래서 디버깅 상태로 DispatcherHookEventArgs를 이거저거 살펴보니 _args 필드가 보이는데, 왠지 이름에서부터 딱 그거라고 하는 듯합니다. ^^

실제로 _args까지 구해서 확인해 보면,

private void Hooks_OperationPosted(object sender, System.Windows.Threading.DispatcherHookEventArgs e)
{
    string name = GetName(e.Operation);
    Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] {name}");

    string target = "System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation+<>c.<.cctor>b__8_0";
    if (name == target)
    {
        Console.WriteLine($"============ {e.Operation.Task.Id} ==============");
                
        System.Delegate method = GetMethod(e.Operation);
        object objArg = GetArg(e.Operation); // 타입이 Action이고, 실제 await 관련 코드를 담은 메서드가 반환됨

        if (method is Action action)
        {
            action();
        }
        else if (method is SendOrPostCallback callback)
        {
            callback(objArg);
        }
    }
}

static object GetArg(DispatcherOperation dop)
{
    FieldInfo fi = typeof(DispatcherOperation).GetField("_args", BindingFlags.Instance | BindingFlags.NonPublic);
    return fi.GetValue(dop);
}

정상적으로 실행되는 것을 확인할 수 있습니다.




그런데, 출력 결과를 보면 약간 이상한 점이 눈에 띕니다.

...[생략]...
[1] System.Windows.Input.InputManager.ContinueProcessingStagingArea
[1] System.Windows.Input.CommandManager.RaiseRequerySuggested
[1] 0 =====Window_Loaded step1=====
[1] System.Windows.Window.<PostContentRendered>b__204_0
[3] System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation+<>c.<.cctor>b__8_0
============ 11 ==============
[3] 0 =====Window_Loaded step2=====
[1] 1 =====Window_Loaded step1=====
[1] System.Windows.Media.MediaContext.RenderMessageHandler
[8] Microsoft.VisualStudio.DesignTools.WpfTap.Utility.DispatcherUtility+<>c__DisplayClass1_0.<SafeInvokeAsync>b__0
[3] System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation+<>c.<.cctor>b__8_0
============ 20 ==============
[3] 1 =====Window_Loaded step2=====
...[생략]...

가만 보면, Window_Loaded가 한 번 더 실행되는 것으로 나옵니다.

게다가, 출력된 스레드 ID를 보면, 3번 스레드에서 "Window_Loaded step2"가 실행되는 것까진 이해를 하겠는데, 뒤이어서 1번 스레드가 마치 "Window_Loaded" 함수를 처음부터 실행하는 것처럼 "Window_Loaded step1"부터 다시 보여주고 있습니다.

도대체 왜 이런 결과가 나오는 것일까요? ^^

사실 두 번 실행되는 것은 맞습니다. 우리가 Hooks_OperationPosted 이벤트로부터 해당 함수를 직접 호출했다고 해서 Dispatcher Queue에 들어간 작업이 없어지는 것은 아니므로, 이후 UI 스레드에 의해서 그 작업이 재차 실행되기 때문입니다.

여기서 문제는, 왜 UI 스레드에 의해 실행된 2번째 단계에서 Window_Loaded를 처음부터 다시 시작하냐는 점입니다.

왜냐하면, Hooks_OperationPosted 단계에서 우리가 직접 실행했던 것은 C#이 만든 IAsyncStateMachine은 상탯값을 "this.<>1__state = -2;"로 설정해 버리기 때문입니다. 즉, 3번 스레드에서 실행한 것으로 인해 상태 머신의 상탯값이 "-2"로 바뀌었고, 그 상태 머신을 (UI 스레드가) 다시 실행하는 과정에서 state의 값이 0이 아니므로 또다시 처음 상태를 반복해 실행하는 것입니다.

예측할 수 있듯이, 위와 같이 진행이 되면 1번(UI) 스레드에서 호출된 상태 머신의 반복으로 계속해서 Window_Loaded step1, Window_Loaded step2 쌍을 이루는 출력이 발생합니다. 그러다 어느 순간 운에 따라, UI 스레드가 Hooks_OperationPosted 내의 callback(objArgs) 코드보다 앞서 실행되면,

...[생략]...
[1] System.Windows.Input.CommandManager.RaiseRequerySuggested
[1] 0 =====Window_Loaded step1=====
[1] System.Windows.Window.<PostContentRendered>b__204_0
[3] System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation+<>c.<.cctor>b__8_0
============ 11 ==============
[1] 0 =====Window_Loaded step2===== // UI 스레드가 먼저 실행
[3] 0 =====Window_Loaded step1===== // callback(arg)로 반복 실행, 이후 await Task.Delay(16)가 Non-UI 스레드에서 실행됐으므로,
[1] System.Windows.Media.MediaContext.RenderMessageHandler
[8] Microsoft.VisualStudio.DesignTools.WpfTap.Utility.DispatcherUtility+<>c__DisplayClass1_0.<SafeInvokeAsync>b__0
[3] 1 =====Window_Loaded step2===== // Dispatcher Queue에 추가되지 않고 스레드풀의 스레드에 의해 마저 처리됨
[1] Microsoft.VisualStudio.DesignTools.WpfTap.Utility.DispatcherUtility+<>c__DisplayClass1_0.<SafeInvokeAsync>b__0
...[생략]...

이번에는 비록 UI 스레드에 의해 상태는 "-2"로 바뀌지만 이후 실행되는 callback(objArgs)가 3번 스레드에 의해 실행, 이어서 내부의 await Task.Delay(16) 코드가 UI 스레드에서 실행된 것이 아니므로 (Dispatcher로 전달되지 않으므로) 재귀적으로 반복 호출되는 구문이 끝나게 됩니다. (원칙만 알면, 설명하지 못할 현상이 없습니다. ^^;)

꽤나 설명이 복잡해졌는데요, 그냥 Hooks_OperationPosted의 인자로 넘어온 값을 이용해 우리도 await 이후의 코드를 호출할 수 있었다는 사실만 알고 넘어가도 되겠습니다. ^^

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




참고로, 본문의 코드에서 await 코드를 다음과 같이 바꾸면,

private async void Window_Loaded(object sender, RoutedEventArgs e)
{
    System.Diagnostics.Trace.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}]==========");
    await Task.Delay(16).ConfigureAwait(false); // 이후 코드를 UI 스레드가 아닌 callback을 받은 스레드풀의 스레드에서 실행
    System.Diagnostics.Trace.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}]==========");
    this.textBox1.Text = "Hello World"; // UI 스레드에서 접근하지 않았으므로 예외 발생
}

이런 출력 결과가 나옵니다.

...[생략]...
[1] System.Windows.Input.CommandManager.RaiseRequerySuggested
[1]==========
[1] System.Windows.Window.<PostContentRendered>b__204_0
[3]==========
Exception thrown: 'System.InvalidOperationException' in WindowsBase.dll
[1] System.Windows.Media.MediaContext.RenderMessageHandler
...[생략]...

즉, await 호출 후 16ms 시간이 지나 callback을 받은 스레드풀의 스레드가 Dispatcher로 작업을 넘기지 않고 직접 호출했기 때문에 System.InvalidOperationException 예외가 발생합니다. 다시 말해, ConfigureAwait(false) 호출로 인해 Dispatcher Queue에도 추가되지 않았으므로 당연히 Hooks_OperationPosted 이벤트도 발생하지 않았습니다.




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







[최초 등록일: ]
[최종 수정일: 3/1/2024]

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

비밀번호

댓글 작성자
 




... 16  17  18  19  20  21  22  23  24  25  26  [27]  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
12964정성태2/11/20227364.NET Framework: 1153. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 avio_reading.c 예제 포팅파일 다운로드1
12963정성태2/11/20228066.NET Framework: 1152. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리 (저해상도 현상 해결)파일 다운로드1
12962정성태2/9/20227893오류 유형: 793. 마이크로소프트 스토어 - 제품이 존재하지 않습니다. 재고가 없는 것일 수 있습니다.
12961정성태2/8/20228040.NET Framework: 1151. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 프레임의 크기 및 포맷 변경 예제(scaling_video.c) [7]파일 다운로드1
12960정성태2/8/20227445개발 환경 구성: 637. ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 세 번째 이야기
12959정성태2/7/20228159.NET Framework: 1150. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 두 번째 이야기 [2]파일 다운로드1
12958정성태2/6/20228232.NET Framework: 1149. C# - ffmpeg(FFmpeg.AutoGen) - 비디오 프레임 디코딩 [2]파일 다운로드1
12957정성태2/6/20227795개발 환경 구성: 636. ffmpeg.exe를 이용해 planar 포맷의 데이터를 packed 형식으로 변환하는 방법? [2]
12956정성태2/4/20227045.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 [2]파일 다운로드1
12955정성태2/4/20226493개발 환경 구성: 635. 비주얼 스튜디오에서 실행하던 ASP.NET Core (.NET Framework) 응용 프로그램을 명령행에서 실행하는 방법 (2)
12954정성태2/4/20226334VS.NET IDE: 173. 비주얼 스튜디오 - Output 창에 색상이 지정된 출력 결과가 "[39m[22m" 식의 문자로 나오는 문제
12953정성태2/2/20226556Linux: 48. Windows 11 + WSL 우분투 GUI 환경에서 한글 출력
12952정성태2/2/20227029.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터 예제(filter_audio.c)파일 다운로드1
12951정성태2/2/20227017.NET Framework: 1147. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터링 예제(filtering_audio.c)파일 다운로드1
12950정성태2/1/20226628.NET Framework: 1146. .NET 6에 추가되지 않은 Generic Math (예: INumber<T>)
12949정성태2/1/20226460.NET Framework: 1145. C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비파일 다운로드1
12948정성태1/30/20226565.NET Framework: 1144. C# - ffmpeg(FFmpeg.AutoGen) AVFormatContext를 이용해 ffprobe처럼 정보 출력파일 다운로드1
12947정성태1/30/20227758개발 환경 구성: 634. ffmpeg.exe - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법
12946정성태1/28/20226271오류 유형: 792. .NET Core - 로컬 개발 중에 docker 호스팅으로 바꾸는 경우 SQL 서버 접근 방법
12945정성태1/28/20226570오류 유형: 791. SQL 서버 로그인 시 localhost는 되고, 127.0.0.1로는 안 되는 문제
12944정성태1/28/20228981.NET Framework: 1143. C# - Entity Framework Core 6 개요
12943정성태1/27/20227822.NET Framework: 1142. .NET 5+로 포팅 시 플랫폼 호환성 경고 메시지(SYSLIB0006, SYSLIB0011, CA1416)파일 다운로드1
12942정성태1/27/20228093.NET Framework: 1141. XmlSerializer와 Dictionary 타입파일 다운로드1
12941정성태1/26/20229486오류 유형: 790. AKS/k8s - pod 상태가 Pending으로 지속되는 경우
12940정성태1/26/20226856오류 유형: 789. AKS에서 hpa에 따른 autoscale 기능이 동작하지 않는다면?
12939정성태1/25/20227559.NET Framework: 1140. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP3 오디오 파일 인코딩/디코딩하는 예제파일 다운로드1
... 16  17  18  19  20  21  22  23  24  25  26  [27]  28  29  30  ...