"Windows Application의 UI 처리" vincent 님 글에 답글을달 수 없어 올려요.

@Vincent 님 UI/UX 쪽에 글(Windows Application의 UI 처리)을 올리셨는데 답글을 달수 없어서 이쪽에 질문 같은 답변 드려요.

매번 불편해도 그냥 내가 이상한거겠거니 하고 넘어가다가, 오늘은 그냥 궁금해서 한 번 의견을 여쭙니다.

  1. (UI Thread가 1개일 경우)위에 예시를 든 대로 아무 Control이나 찍고 Invoke를 사용하는 것이 하나의
    Window에 대해 Invoke를 사용하는 것과 정말 같을까요? 전 그렇게 알고 있는데 혹시나 싶어 확인차 여쭤봅니다.
  2. 그렇다면 어차피 같은 동작을 보일텐데 Control마다 Invoke가 가능하도록 열어준 이유는 무엇일까요?
    private으로 노출을 안시켰다면 코딩하는 사람들이 오용하지 않았을텐데 말이죠.

어떤 분이 잘 정리하신 글을 찾아서 먼저 공유 드립니다.

Windows Forms이든 WPF든 UI 스레드 접근을 돕기 위한 SynchronizationContext 가 준비되어 있는 것으로 알고 있어요.

  • Winform: WindowsFormsSynchronizationContext
  • WPF: DispatcherSynchronizationContext

그리고 각 컨트롤의 Invoke 또는 BeginInvoke 함수를 노출 한 것은 UI 스레드의 접근성을 높이기 위한 설계로 이해했는데요.
예를 들어서 어떤 버튼 Click 이벤트를 처리하는데 이벤트 처리 함수 내에서 API 호출을 위한 비동기 처리를 하였다면 컨트롤에 직접 결과를 반영할 수 없기 때문에 WindowsFormsSynchronizationContext를 통해 UI 스레드의 특정 컨트롤로 접근해야 하는데 이때 이벤트 처리 함수에 전달된 sender 컨트롤의 Invoke를 사용할 수 있다면 간결하게 처리가 가능할 것 같습니다.
근데 Invoke 함수를 어디서든 부를수 있기 때문에 UI 처리와 비즈니스 로직이 코드 비하인드에서 혼재되는 양상이 발생될 수 있겠네요.

7개의 좋아요

1번에 대해서 저는 똑같다고 알고있습니다. 그래서
button.Invoke => 절대 이렇게 사용안하고 바로 Invoke 로 사용하고요…
개인적으로 Invoke는 거의 사용안하고 BeginInvoke 를 사용합니다.
외부 쓰레드에서 Invoke를 사용했을때 폼 close 할때 Invoke 부분에서 무한 블러킹에 걸리는 현상이 있더군요.

3개의 좋아요

답변 감사드립니다.

카카오톡 닷넷 채팅방에서도 어제 질문을 올리고 의견을 들어봤었는데, 답이라고 생각한 부분이 있었습니다.

저는 우선 UI Thread가 1개인 경우만 생각하고 있었고, 경험이 풍부하지 못해서 UI Thread가 여러 개일 경우를 전혀 고려하지 않았습니다.

스레딩 모델 - WPF .NET Framework | Microsoft Docs

위 글에서 맨 처음에 UI Thread가 여러개 필요한 경우에 대한 예시를 들어주고 있습니다.

WPF 개발자는 두 개 이상의 스레드를 사용하는 인터페이스를 작성할 필요가 없습니다. 다중 스레드 프로그램은 복잡하고 디버그하기 어려우므로 단일 스레드 솔루션이 있을 경우 피해야 합니다.
하지만 UI 프레임워크는 아무리 설계를 잘하더라도 문제의 모든 부분을 해결하는 단일 스레드 솔루션을 제공할 수 없습니다. WPF가 근접하지만, 스레드를 여러 개 사용해야만 UI(사용자 인터페이스) 응답성이나 애플리케이션 성능이 개선되는 상황이 여전히 있습니다. 일부 배경 자료를 설명한 후 이 문서에서는 이러한 상황 중 일부를 살펴보고 몇몇 하위 수준 세부 정보에 대한 설명으로 마무리 짓습니다.

따라서 UI Thread가 1개 일 경우에는

WPF의 DispatcherObject에서 직접 Invoke를 하는 것, Winform에서 Control.Invoke를 하는 것에는 차이가 없다고 봐야하고 여러 개 일 경우에는 의미가 있다고 볼 수 있습니다.

추가적으로 UI Thread가 여러개인지는 아래 코드를 검색하여 몇 번 호출되었는지 여부에 따라 UI Thread의 개수를 확인 가능할 것입니다.

Winform에서는

Application.Run()

WPF에서는

var dispatcher = Dispatcher.CurrentDispatcher; 
var context = new DispatcherSynchronizationContext(dispatcher); 
SynchronizationContext.SetSynchronizationContext(context); 
dispatcher.Run()

WPF 예시코드는 @aroooong 님께서 주셨습니다. 감사합니다.

따라서 위 코드를 검색해보고 1번밖에 호출되지 않았다면 Control.Invoke / DispatcherObject.Dispatcher.Invoke를 Control이 아닌 해당 Form(this) 또는 Window를 찍고 일관성있게 코딩하는 것이 추후 발생할 수 있는 휴먼에러를 방지할 수 있는 방법이라 생각됩니다.

관심가져주셔서 감사드립니다.

어제 찾아보면서 도움이 되었던 링크들을 레퍼런스용도로 첨부합니다.
WPF의 Threading - Dispatcher, DispatcherObject
병렬 컴퓨팅
동기화 컨텍스트의 이해 Part 1
동기화 컨텍스트의 이해 Part 2

2개의 좋아요

우선, "Windows Application은 COM으로부터 시작"되었다는 말부터 오류가 있습니다. Windows Application은 COM과 전혀 상관없이 작성할 수 있고, 단지 COM 개체를 사용한 경우 그것의 Apartment를 지정하는 것이 필요하기 때문에 닷넷의 경우 STAThread 특성을 지정하는 것뿐입니다.

만약 닷넷 윈폼에서 COM 개체를 사용하지 않는다면, 그것도 STA 지원 COM 개체를 사용하지 않는다면 STAThread 특성은 생략 가능합니다.

그리고, 질문이 잘 눈에 안 들어오는데 Control.Invoke와 Form.Invoke를 왜 나눴느냐는 의미인 건가요? (그렇다고 가정하고 답변을 드리면) 그건 나눈 게 아니고 Form이 Control을 상속받고 있으며 Invoke가 Control에 구현되었기 때문에 그런 것입니다. 따라서, 어느 Invoke를 사용하든 동작은 완전히 같습니다.

8개의 좋아요

이미 정리가 되셨다니 잘 되었네요.

다른분들도 보시는 글이라서 오해가 될 수 있는 부분에 대해서 말씀드리자면,
UI 스레드가 2개 이상인 환경은 존재하지 않는 것으로 알고 있습니다.
1개의 UI 스레드에서 Task, ThreadPool, async await 등으로 비동기 처리를 위한 스레드 접근시 나머지 스레드는 UI 스레드가 아닌 WORKER 스레드이고, WORKER 스레드에서 UI 객체 접근에 따른 크로스 스레드 이슈가 발생 되지 않도록 Invoke(동기), BeginInvoke(비동기)와 같은 인터페이스를 제공해 WORKER 스레드가 아닌 UI 스레드에서 UI 처리의 안전한 환경을 구성할 수 있게 되는 것으로 알고 있습니다.

2개의 좋아요

아하 그렇군요.

제가 표현한 것은 "Windows Application은 COM으로부터 시작"되었다는게 아니고
"Windows Application은 COM으로 부터 시작되어 전통적으로 STA"라는 것을 말한건데 이것도 내용이 틀렸네요…

아래 링크 글에서 인용된 부분을 잘못 이해해서 그렇게 표현했습니다.

WPF의 Threading - Dispatcher, DispatcherObject : 네이버 블로그 (naver.com)

WPF의 Threading은 **STA(Single-Threaded Apartment)**를 기본으로 한다.
STA라는 용어는 COM에서 사용된 것으로서, STA 객체는 하나의 Thread(보통 그 객체를 생성한 Thread)에서만 엑세스 할 수 있도록 하는 규칙 및 제한을 의미 한다.

이것은 제가 잘못이해한 것이고, 바로 잡아주셔서 감사합니다.


제가 질문 드린 내용은

UI Thread가 여러개일 경우를 고려하지 않고 1개일 경우만을 생각하여, Windows App이 UI Thread가 단 1개만 존재한다고 가정했을 때 왜 개발자들에게 노출 가능하도록 했냐는 것입니다.

Winform의 경우에 말씀하신 것 같이 Control이나 Form에서 invoke를 하는 것이 같은 효과를 지닌다는 것은 저도 알고 있습니다.

근데 단순 제 생각에 ‘코드가 다르면 동작도 달라야한다’ 라는 제 관점이 Control을 상속받았다고 해서 Control과 Form에서 이곳저곳에서 접근이 가능한데, 그것이 모두 동작이 같다는 것이 마음에 들지 않아서 푸념식으로 했던 것이었습니다.

아니면 이곳저곳의 Control 클래스를 상속받은 모든 객체들에게 가능하게 해준 이유가 있었는지…(동작이 같더라도요…)

약간 떼를 쓰는거지만…차라리 Control.Invoke가 아닌 전혀 다른 객체에서’만’ Invoke가 가능하게하고 그것이 sealed 클래스였다면 코드도 똑같이, 동작도 똑같이 보였을거라는 생각 때문에 질문을 올렸던 것입니다.

위 생각들 때문에 질문을 올렸는데, 카카오톡 닷넷 채팅방에서 ‘파브’ 닉네임을 지닌 분께서 말씀하시길,

내가 작성한 UI 컨트롤이 다른 UI Thread에서 동작하고 있을 경우를 생각하라

고 하셔서 납득을 할 수 있었습니다.
일반적으로 UI Thread가 1개인 것이고, UI Thread가 여러 개인 경우라면 Control에 현재 참조하고 있는 컨텍스트를 캡처해서 자기를 생성한 UI Thread가 누구인지 알고 거기서만 접근가능하도록 하는 방식이 이해가 되었기 때문입니다.

UI Thread가 다르면 컨텍스트도 다를 것이기 때문입니다.


최대한 제 생각을 정리하고 인과를 정리해 봤는데, 이해가 되셨을지 잘 모르겠습니다…ㅎㅎ 감사합니다!!

1개의 좋아요

디스패처를 갖는 스레드를 여러개 만들면
그것이 UI스레드 아닌가요? ㅎㅎ

윈폼에선 Application.Run(); ← 에 해당 되구요

실제로 윈폼 기준으로

ThreadPool.QueueUserWorkItem( (p) =>
{
        Form form = new Form();
        Application.Run(form);
}, null);

으로 새로운 UI스레드를 만들 수 있습니다.

3개의 좋아요

UI 스레드가 2개 이상인 환경은 존재하지 않는 것으로 알고 있습니다.

환경이 존재하지 않는다는 말씀이 일반적으로 Windows Application들에 UI Thread가 여러개일리가 없다고 하시는건가요?

아니면 개발자체가 UI Thread는 여러개로 개발자가 코딩할 수 없다고 하시는건가요?

만약 후자라면 저는 틀리게 알고 있으니 정리를 제대로 한 게 아닙니다…

반문이라고 하긴 좀 그렇지만 제가 링크드린 글에 병렬 컴퓨팅 아티클에 보면 이런 문구가 있습니다.

No matter how well architected, however, no UI framework will ever be able to provide a single-threaded solution for every sort of problem. WPF comes close, but there are still situations where multiple threads improve user interface (UI) responsiveness or application performance.

영알못이라 구글 번역기를 돌려보자면

그러나 아무리 잘 설계된 UI 프레임워크도 모든 종류의 문제에 대해 단일 스레드 솔루션을 제공할 수 없습니다. WPF에 근접했지만 여러 스레드가 UI(사용자 인터페이스) 응답성 또는 응용 프로그램 성능을 향상시키는 상황이 여전히 있습니다.

라고 해석됩니다.

만약 의미가 저게 맞다면 MSDN에서도 여러개의 UI Thread를 통해 응답성을 향상시키는 것을 글로서 알려주고 있습니다.

혹시 제가 의미를 잘못해석한걸까요?

3개의 좋아요

네 말씀이 맞겠네요.

3개의 좋아요

같은 맥락인지는 모르겠으나, UI Thread말고, Messsage Loop는 동시에 1개만 가능한 것으로 알고 있습니다.

옛날에 만든 예제인데,

WinformExample/ApplicationRunOrFormShowDialogTest at master · christian289/WinformExample (github.com)

이걸 조금 수정했습니다.

static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            Form1 MainForm1 = new Form1("Form1.Show()로 생성");
            Form1 MainForm2 = new Form1("Application.Run()으로 생성");

            MainForm1.Show(); // Main UI Thread의 Message Loop 사용

            Application.Run(MainForm2); // Main UI Thread의 Message Loop 사용

            // Application.Run 에서 Message Loop를 잡고 있음.
            // 따라서 Form.ShowDialog 처럼 밑에 줄 코드로 내려가지 않음.
            // Application.Run으로 실행된 Form이 종료되거나 Application.Exit()가 호출되면 ApplicationExit 이벤트핸들러가 동작.
            // 이후 MessageLoop 종료되고 밑에 Form.Show는 예외가 발생함. 왜냐하면,
            // Show는 Show를 호출한 Thread의 MessageLoop를 공유하는데 ApplicationExit의 호출로 Main Thread의 MessageLoop가 종료되었기 때문

            MainForm1 = new Form1("Exception 폼"); // Exception 발생
            //MainForm1.Show(); // Main UI Thread의 Message Loop 사용
            Application.Run(MainForm1);

            // 여담으로.. Main 함수에서 Application.Run을 사용하지 않고 Form1의 인스턴스를 할당하여 ShowDialog로 띄우고,
            // Form1에서 Application.Run을 해도 예외가 발생한다. 두번째 메세지 루프를 사용할 수 없다는 것인데,
            // Main Thread의 메세지 루프가 실행되지도 않았는데 Application.Run이 동작을 안한다는 것은 이해가 안된다.
        }
    }

위 소스를 실행시키면

Application.Run이 처음 호출된 시점에서 코드가 더 이상 밑으로 진행되지 않은 상태로 Form 2개를 모두 조작할 수 있습니다.
이후 Application.Run으로 시작한 Form을 종료하면 코드가 밑으로 진행됩니다.
저는 이것을 Message Loop가 종료된 것으로 이해했습니다.

이후 다시 밑에서 Form을 Show() 하지만 Message Loop는 이미 종료되었기 때문에 더이상 동작하지 않고 프로세스는 종료되는 것 같고, Show가 아닌 Application.Run으로 다시 실행하면 Message Loop가 다시 동작하게 되어 추가된 1개의 Exception Form을 조작할 수 있습니다.


그런데 정리하다보니 Message Loop던 UI Thread 던 동시에 1개는 존재할 수 없는 것 같군요.
이게 맞는지 모르겠습니다.
의문점 던져주셔서 감사드립니다.

2개의 좋아요

어렵게 생각할 건 아닌거 같아요. =ㅂ=

그냥 편의성 때문이지 않을까요?

만약 invoke 기능이 control 이나 dispatcher 노출되어 있지 않다면 어떻게 제공되어야할까… 를 생각해보면

다양한 상황에서 모두를 수용할 수 있는 public 한 구조로 노출되어야할 거 같은데

이건 사용하는 사람에게 편의성도 없으면서 혼란을 더 가중시킬 수도 있어 보여요.
(일일 제어 코드를 넣어야하는 불편함이 생기겠죠. 또 언제 어디서 사용해야하는지도 고민해야할 거구요.)

저도 잠깐 비슷한 의문을 품었던 적이 있었지만, 결론은 앞서 말한대로 였어요.


그리고 이런 고민을 하신다는 건

@Vincent 님께서 좀 더 OOP 지향적이고 구조 안정적인 사고를 하고 계신다는 방증일 겁니다.

화이팅! ㅇㅅㅇ/

4개의 좋아요

감사합니다!!

2개의 좋아요

제가 알고있기론 다중 UI 쓰레드를 이용했던 프레임워크들이 없던건 아니라고 알고있습니다.

다만, UI 쓰레드들 간의 동기화가 힘들다는 이슈로 마이크로소프트에서는 기술적 문제가 아닌

정책적으로 1개의 UI 쓰레드를 가져간다고 알고있습니다.

제가 알고 있는건 이런 내용인데 혹시 이것에 대해 자세히 알고 계신 분들 계신가요?

3개의 좋아요

UI 프로세스 안에 UI Thread가 없을 수 없기 때문에 Windows Message Loop에 메세지를 전달할 목적으로 기본으로 1개를 제공한다는 것이지, 2개 이상을 사용하지 마라고는 하지 않은 것으로 알고 있습니다.

그리고 말씀하신 동기화 부분은 캡슐화가 잘 되어있다는 가정 하에 해소할 수 있는 문제가 아닐까 조심스레 생각해봅니다.

어차피 Control은 View를 담당하는 것으로 데이터는 다른 곳에 있어야 패턴상 맞고, 객체지향적으로도 책임을 분리할 수 있을 거라는 생각 때문입니다.

그렇기 때문에 제가 경험해보지 못한 성능에 관한 이슈로는 UI Thread를 다수 둘 수도 있겠다라고 생각하지만, 동기화는 잘 모르겠습니다.

2개의 좋아요

Invoke에 대한 논점이 옮겨 붙은 것 같은데요.

UI 스레드가 2개 이상인 환경은 존재하지 않는 것으로 알고 있습니다.

충분히 설명 드리지 못한 것 같네요.
말씀처럼 일반적인 상황에서 UI 스레드를 여러개 띄우지는 않아 그렇게 표현하였습니다.
WPF 어플리케이션에서 스플래시를 통한 어플리케이션 Startup 타임을 줄이기 위해 일시적으로 UI 스레드를 여러개 띄우는 경우는 봤지만 런타임 상황에서 UI 스레드를 여러 개 띄워 놓고 운용하는 모습을 본 적은 없습니다.
@마수리 님이 언급하신 것처럼 UI 스레드를 여러개 띄우면 각 UI 스레드에서 만들어진 UI 객체들은 인터렉션을 위해 동기화와 같은 추가적인 작업이 필요하여 복잡성만 높아질 것 같습니다.
그래서 하나의 UI 스레드 + 나머지 WORKER 스레드로도 복잡한 어플리케이션을 작성하기에 충분한 것 같습니다.
참고로 자사 퍼플 런처도 그렇게 작성 하였어요~
좋은 밤 되세요.

4개의 좋아요

음 저도 제 질문에 표현이 부족했나보네요.

저도 여지껏 UI Thread 1개로 일관되게 작업했고 2개 이상의 경우는 고려조차 하지 않았습니다.

그러다가 왜 Dispatcher나 Invoke를 여러군데에서 가능하도록 한건지 의도를 여쭈고자 이 질문을 만든 것입니다.

애초에 UI Thread가 무조건 1개로만 가능했다면 노출조차 하지 않는 게 맞다고 봅니다.

하지만 퍼포먼스를 위해 2개 이상은 가능하다고 되어있는 MSDN글을 링크 드린 것입니다.

저도 UI Thread가 2개 이상인 경우는 본적이 없습니다.

답변 감사드립니다.

4개의 좋아요

음…
윈폼은 자세히 모르겠는데 wpf는ui를 처리하는
render thread가 별도 존재합니다.
(render thread와 ui thread는 서로 다릅니다)

따라서 UI 생성시점에 대한 동기처리는딱히 고려해야할 부분은 업ㅈㅡㄹ것 같다는 생각이고
말씀하신 인터렉션에 대한 동기? 처리라는것도 각 개별 ui스레드에서 메세지펌핑 역할 하는 부분에 있어 동기를 고려해야 하는 상황이 과연 어떤게 있을지 의문입니다

해당 글에 대한 반박? 하려는 주장은 아니고 그냥 제 짧은 개인적인 생각을 댓글로 남겨보았습니다

5개의 좋아요

간단히 시나리오를 만들어 봤습니다~

<StackPanel Orientation="Vertical">
        <Button Content="New Window with UI Thread" x:Name="NewWindowWithThread" Click="NewWindowWithThread_Click" />
        <Button Content="New Window" x:Name="NewWindow" Click="NewWindow_Click" />
        <Button Content="Change Text" x:Name="ChangeText" Click="ChangeText_Click"/>
        <TextBlock x:Name="Result" Text="Hello" />
    </StackPanel>
/// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private MainWindow _parent;

        public MainWindow()
        {
            InitializeComponent();
        }

        public MainWindow(MainWindow parent) : this()
        {
            _parent = parent;
        }

        private void ChangeText_Click(object sender, RoutedEventArgs e)
        {
            var window = _parent ?? this;
            window.Result.Text = "World";
        }

        private void NewWindow_Click(object sender, RoutedEventArgs e)
        {
            var w = new MainWindow(this);
            w.Show();
        }

        private void NewWindowWithThread_Click(object sender, RoutedEventArgs e)
        {
            var thread = new Thread(() =>
            {
                var w = new MainWindow(this);
                w.Show();

                w.Closed += (sender2, e2) => w.Dispatcher.InvokeShutdown();

                System.Windows.Threading.Dispatcher.Run();
            });

            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
        }
    }
  • NewWindowWithThread 버튼: 새로운 UI THREAD와 함께 자식 윈도우를 보여주면서 현재 윈도우를 참조로 전달합니다.
  • NewWindow: 자식 윈도우를 보여주면서 현재 윈도우를 참조로 전달합니다.
  • ChangeText: 부모 윈도우의 TextBlock 값을 변경합니다.

NewWindow 버튼을 선택하여 자식 윈도우가 만들어 ChangeText 버튼을 선택하면 같은 UI 스레드를 공유하므로 값이 변경 됩니다.
NewWindowWithThread 버튼을 선택하여 자식 윈도우를 만들어 ChangeText 버튼을 선택하면, 서로 다른 UI 스레드의 엘리먼트를 접근하게 되어 오류가 발생 됩니다.
image
이를 해결하기 위해서 부모 윈도우 UI 스레드의 Dispatcher 인스턴스에 접근하여 UI 처리를 요청합니다.

var window = _parent ?? this;
window.Dispatcher.BeginInvoke(() =>
{
    window.Result.Text = "World";
});

이 작업을 동기화라고 말씀드렸어요~

3개의 좋아요

아 render thread에 대해서는 잘 몰랐는데 한번 공부해볼 필요가 있겠네요.

그럼 혹시 왜 마이크로소프트는 자사의 프래임워크에 왜 1개의 UI쓰레드만 강제했을지 혹시 생각해보신 바가 있으시면 공유 부탁드려도 될까요?

2개의 좋아요

Dispatcher Invoke 처리를 동기화 라고 표현하신거군요?

동기화라고 말씀하셔서 다른 ui 화면에서 어떤 동기적 처리가 필요한 상황이 있을까 의문이었습니다

그런데 Dispatcher Invoke는 비단 멀티 UI Thread뿐 아니라

다수의 Work Thread 에서 ui 접근시에도 사용되어 처리되는 당연시 "필연"적인 방법 아닌가요?

다시말해 UI Thread가 여러개인 환경에서만 고려할부분은 아닌거 같아보여서요

4개의 좋아요