WPF UI 스레드 안전하게 다루기: Invoke와 BeginInvoke의 차이와 활용법

이 Post 는 WPF 에서 UI 스레드에 안전하게 접근하는 방법을 알아보고 Invoke 와 BeginInvoke 의 차이를 알아봅니다.

WPF 로 개발을 할 때 스레드에서 UI 에 접근할려고 하면 다음과 같은 오류를 만납니다.

외부에서 생성된 스레드에서 UI 스레드를 제어하려고 하면 발생하는 오류 입니다.

해결책은 다양한 방식이 있지만 대부분 Dispatcher 를 사용하게 됩니다.

창 전체 혹은 UserControl 전체에 대해서 제어를 할려면 보통 해당 창에서 this.Dispatcher 를 사용합니다.

특정 컨트롤만 제어 하기 위해선 controlName.Dispatcher 를 사용하면 됩니다.

혹은 접두 없이 DIspatcher 를 사용하기도 하는데 이는 현재 스레드를 뜻합니다. 즉 this.Dispatcher 와 동일하다고 보시면 됩니다.

다음과 같이 정리하면 될 것 같습니다.

방식동작사용 권장 상황
this.Dispatcher현재 Window가 속한 UI 스레드의 Dispatcher를 사용합니다.현재 창의 UI 스레드에서 작업을 예약할 때
textBox.DispatcherTextBox가 생성된 스레드의 Dispatcher를 사용합니다.특정 컨트롤의 스레드에 작업을 전달할 때
Dispatcher현재 스레드의 Dispatcher를 사용합니다.위험: 백그라운드 스레드에서 호출 시 실패
Application.Current.Dispatcher메인 UI 스레드의 Dispatcher를 명시적으로 사용합니다.컨트롤 없이 UI 스레드에 안전하게 접근할 때

Dispatcher 이후에는 Invoke 혹은 BeginInvoke 를 사용하게 되는데요, 그 차이는 다음과 같습니다.

테스트를 위한 간단한 코드를 작성 했습니다.

1. MainWindow.xaml

<Window x:Class="BlogDemo00.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BlogDemo00"
        mc:Ignorable="d"
        Title="MainWindow" Height="80" Width="200">
	<Grid>
		<TextBlock x:Name="txtMain" Text=""
				   HorizontalAlignment="Center" 
				   VerticalAlignment="Center" 
				   FontSize="32" 
				   Foreground="DarkBlue" 
				   FontWeight="Bold"/>
	</Grid>
</Window>

2. MainWIndow.xaml.cs

using System;
using System.Threading.Tasks;
using System.Windows;

namespace BlogDemo00
{
	/// <summary>
	/// MainWindow.xaml에 대한 상호 작용 논리
	/// </summary>
	public partial class MainWindow : Window
	{
		public MainWindow()
		{
			InitializeComponent();

			this.Loaded += MainWindow_Loaded;
		}

		private void MainWindow_Loaded(object sender, RoutedEventArgs e)
		{
			Task task = new Task(worker);
			task.Start();
			Console.WriteLine("작업이 시작되었습니다. UI는 계속 응답합니다.");
		}

		private void worker()
		{
			bool? IsSync = null; // 동기 실행 여부

			Console.WriteLine($"IsSync: {IsSync}");

			for (int i = 0; i < 3; i++)
			{
				if (IsSync == null)
				{
					txtMain.Text = $"Count: {i}";
					System.Threading.Thread.Sleep(10);
					Console.WriteLine($"Count: {i} - UI 스레드에서 실행됨");
				}
				else if (IsSync.Value == true)
				{
					// 동기 실행: UI 스레드에서 실행
					Dispatcher.Invoke(() =>
					{
						txtMain.Text = $"Count: {i}";
						System.Threading.Thread.Sleep(10);
						Console.WriteLine($"Count: {i} - UI 스레드에서 실행됨");
					});
				}
				else if (IsSync.Value == false)
				{
					// 비동기 실행: UI 스레드에서 실행
					Dispatcher.BeginInvoke(new Action(() =>
					{
						txtMain.Text = $"Count: {i}";
						System.Threading.Thread.Sleep(10);
						Console.WriteLine($"Count: {i} - UI 스레드에서 실행됨");
					}));
				}

				// 1초 대기
				Console.WriteLine($"Count: {i} - 백그라운드 스레드에서 실행됨");
				System.Threading.Thread.Sleep(10);
			}

			Console.WriteLine("작업이 완료되었습니다. UI는 계속 응답합니다.");
		}
	}
}

코드를 보시면 아시겠지만 Invoke 는 동기, BeginInvoke 는 비동기로 내부 코드를 실행합니다.

실행 결과 확인 하겠습니다.

1. Invoke (동기)

예상 하셨듯이 정상적으로 실행 됩니다.

2. BeginInvoke (비동기)

위에 코드를 보면 아시겠지만 i < 3 이기 때문에 “Count: 3” 이 원래 대로라면 나올 수가 없습니다.
또한 for 문을 다 돌고 난 후에 출력한 “작업이 완료되었습니다…..” 이후에 “Count: 3” 이 찍혔네요.
즉, 마지막 구분은 for 문을 다 돌고 난 후에 진입하여 실행이 되었단 말입니다.
이와 같이 BeginInvoke 는 비동기로 동작 하기 때문에 사용할 때는 순서가 필요 없는 작업일 때만 써야 하는 주의가 필요 합니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤