※ MSDN 으로 결론은 내었지만 본문은 본인이 적어놓고도 제대로 이해하지 못한 글입니다. 읽을 때 주의바랍니다. |
C# 윈도우즈 프로그래밍을 하다보면 메인 쓰레드가 아닌 다른 쓰레드에서 메인 쓰레드에서 생성된 컨트롤을 제어해야하는 상황이 온다.
하지만, 메인 쓰레드에서 생성된 컨트롤을 다른 쓰레드에서 제어하려고 하면 아래와 같은 현상이 일어난다.
public partial class MainWindow : Window
{
private ProgressBar progressBar;
public MainWindow()
{
InitializeComponent();
progressBar = new ProgressBar();
progressBar.Maximum = 100;
this.Content = progressBar;
Thread thread = new Thread(this.threadEntryPoint);
thread.Start();
}
public void threadEntryPoint()
{
for(int i=1;i<=5;i++)
{
Thread.Sleep(200);
progressBar.Value = (i * 20);
}
}
}
이런 현상은 왜 발생하는 것인가?
Control 클래스가 상속되어 있는 녀석을 따라가다보면 DispatcherObject 클래스를 만나볼 수 있다.
docs.microsoft.com/en-us/dotnet/api/system.windows.controls.progressbar?view=net-5.0
DispatchObject 내에는 Dispatcher 라는 프로퍼티가 정의되어 있어
해당 오브젝트(컨트롤) 생성된 스레드에서 조작되고 있는지를 확인하는 루틴과
오브젝트가 생성된 쓰레드에서 메시지 큐 처리를 시키게 한다.
public partial class MainWindow : Window
{
private ProgressBar progressBar;
public MainWindow()
{
InitializeComponent();
StackPanel stackPanel = new StackPanel();
Button button;
// 버튼 생성
button = new Button();
button.Content = "Current Thread";
button.Click += CurrentThread_ButtonClick;
stackPanel.Children.Add(button);
button = new Button();
button.Content = "Other thread";
button.Click += OtherThread_ButtonClick;
stackPanel.Children.Add(button);
button = new Button();
button.Content = "Other thread exception";
button.Click += OtherThread_Exception_ButtonClick;
stackPanel.Children.Add(button);
this.Content = stackPanel;
}
public void CurrentThread_ButtonClick(object sender, EventArgs e)
{
var obj = new TestDispatchObjectClass();
obj.Do();
}
public async void OtherThread_ButtonClick(object sender, EventArgs e)
{
var obj = new TestDispatchObjectClass();
await Task.Run(async () =>
{
if (!obj.CheckAccess())
{
await obj.Dispatcher.InvokeAsync( () => obj.Do());
}
});
}
public async void OtherThread_Exception_ButtonClick(object sender, EventArgs e)
{
try
{
var obj = new TestDispatchObjectClass();
await Task.Run( () => obj.Do() );
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
}
public class TestDispatchObjectClass : DispatcherObject
{
public void Do()
{
this.VerifyAccess();
Debug.WriteLine("TestDispatchObjectClass::Do");
}
}
위 소스를 실행하면 Current Thread, Other thread, Other thead exception 의 총 3개의 버튼이 실행된다.
제일 먼저 아래에 있는 TestDispatchObjectClass 내에 Do 함수의 VerifyAccess 함수는
해당 오브젝트가 생성된 쓰레드에 묶여있는지 확인하고 묶여 있는 경우 InvalidOperationException 을 발생시킨다.
먼저, CurrentThread_ButtonClick 이벤트 함수를 보면
오브젝트가 생성된 쓰레드에서 직접적으로 실행시키기 때문에 정상적으로 실행이 된다.
OtherThread_ButtonClick 이벤트 함수의 경우 Task 클래스를 통해 다른 쓰레드를 생성시켜 실행시키고
CheckAccess 메소드를 통해 현재 쓰레드가 DispatcherObject 에 묶여있는지 확인 후 묶여있지 않은 경우
Dispatcher 를 경유해서 InvokeAsync 를 통해 실행시킨다.
마지막으로 OtherThread_Exception_ButtonClick 의 경우
VerifyAccess 를 통해서 InvalidOperationException 이 나타난다.
원문을 몇번을 다시 읽어봐도 썩 이해가 안가서 MSDN 을 찾아보고 정리하면 아래와 같다.
docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcher.verifyaccess?view=net-5.0
Dispatcher.VerifyAccess Method (System.Windows.Threading)
Determines whether the calling thread has access to this Dispatcher.
docs.microsoft.com
docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcher.checkaccess?view=net-5.0
Dispatcher.CheckAccess Method (System.Windows.Threading)
Determines whether the calling thread is the thread associated with this Dispatcher.
docs.microsoft.com
요약하면, 오브젝트를 생성한 쓰레드에서 실행 가능한지 확인하려면 VerifyAccess
생성된 오브젝트의 쓰레드가 해당 Dispatcher 과 관련이 있는지 확인하기 위한 것은 CheckAccess
즉, 확실한 실행이 보장되어야 하는 경우에는 VerifyAccess (throw exception)
그것이 아니라면 CheckAccess (bool)
CheckAccess 에 대해서 뇌피셜적으로 조금더 설명하면
A.exe 라는 다른 프로세스에서 생성된 메인 쓰레드와 B.exe 라는 또다른 프로세스의 메인 쓰레드가 있을 경우
A.exe 에 있는 컨트롤을 B.exe에서 제어 하고자 할 때 B.exe 메인 쓰레드는 A.exe 메인 쓰레드에서 생성된 자식쓰레드가 아니기 때문에 CheckAccess 는 false 가 나올 가능성이 높아보인다.(즉, 관련이 없다.)
'Windows > C#/WPF' 카테고리의 다른 글
이벤트 핸들러 (0) | 2021.05.07 |
---|---|
DependencyObject (0) | 2021.05.07 |
솔루션 내에 다른 프로젝트의 리소스를 얻어오는 방법 (0) | 2021.05.04 |
Property resource 로 등록된 이름을 불러오는 방법 (0) | 2021.05.04 |
WPF Control (0) | 2021.05.02 |
댓글