[C# WPF] WPF 멀티 쓰레드, BackgroundWorker
https://www.youtube.com/watch?v=KfY6DqWtcqs&list=PLxU-iZCqT52Cmj47aKB1T-SxI33YL7rYS&index=4
📌 1. 멀티 쓰레드 프로그래밍
- 멀티쓰레드 : 여러개의 쓰레드가 동시에 특정 코드블럭을 실행하는 것
ex) 채팅 등
- 모든 WPF 프로그램은 최소한의 랜더링을 위한 백그라운드 쓰레드와 UI 쓰레드 ( UI 인터페이스 관리) 두개의 쓰레드로 기동된다.
UI 스레드는 사용자 입력을 받고 화면을 그리고 코드를 실행하고 이벤트 등을 처리
- WPF는 기본적으로 STA(Single Thread Apartment) 모델을 지원하는데, 하나의 쓰레드는 전체 응용프로그램에서 실행되고 모든 WPF 객체를 소유하고 있고 TextBox와 같은 WPF UIElements 요소들은 쓰레드 선호도라는 것이 있어 다른 쓰레드와 상호작용을 할 수 없다.
- 쓰레드 선호도
: 화면을 그리는 쓰레드는 컨트롤들을 소유하고 다른 쓰레드에서는 직접 접근할 수 없도록 되어있는 것
- WPF에서 멀티 쓰레드 처리를 위해서 Dispatcher를 이용 할 수도 있고 Background Worker를 사용 할 수도 있다.
📌 2. Background Worker를 이용한 WPF 멀티쓰레드 프로그래밍
- Windows 응용 프로그램 멀티 쓰레딩에서 가장 어려운 개념은 다른 쓰레드에서 UI를 변경 할 수 없다는 것.
대신 UI 쓰레드에서 메서드를 호출해야 원하는 변경이 이루어 진다.
- 백그라운드 워커 (BackGround Worker) : System.ComponentModel 아래의 클래스로 코드를 별도의 쓰레드에서 동시에 비동기적으로 실행하게 해 주는데 응용프로그램의 기본 쓰레드와 자동으로 동기화 해 준다.
호출 쓰레드는 정상적으로 실행이 되고 Background Worker는 백그라운드에서 비동기적으로 실행된다.
- 백그라운드에서 작업을 실행하고 UI 실행 등을 연기하는 데 사용되는데 사용자는 UI가 계속 반응하기를 원하면서 데이터를 다운로드 한다던지 오래 걸리는 작업이 있어 진행사항을 표시해야하는 경우, DB 트랜잭션 처리 등에 유용하다.
- BackGround Worker에서 일어하는 작업에 대해 변경이 생길 때 호출되는 ProcessedChanged 이벤트, 작업이 완료 되었을 때 지원하는 RunWorkerCompledted 이벤트가 발생한다.
- DoWork 이벤트에서 백그라운드 쓰레드가 할 일을 기술 하는데 DoWork 이벤트 처리 메서드 내용은 다른 백그라운드와 다른 쓰레드에서 처리되므로 UI쪽을 접근 할 수 없다.
그래서 ReportProgress() 메서드를 호출하면 ProcessChanged 이벤트가 발생하여 UI를 업데이트를 할 수 있다..
- ProgressChanged 및 RunWorkerCompleted 이벤트는 BackgroundWorker가 만들어지는 것과 동일한 쓰레드에서 실행된다.
- BackgroundWorker는 일반적으로 기본/UI 쓰레드이므로 UI를 업데이트 할 수 있다.
따라서 실행중인 백그라운드 작업과 UI간에 수행할 수 있는 유일한 통신 방법은 ReportProgress() 메서드를 사용하는 것이다.
- DoWork 이벤트 처리 메서드 내부에서 파라미터가 필요하면 백그라운드 워커를 호출하는 RunWorkerAsync()
메서드의 인수로 넣어주면된다.
int count = (int) e.Argument;
- DoWork 이벤트 처리 메서드 내부에서 e.Result 등으로 어떤 결과 값을 넣어두면 RunWorkerCompledted 이벤트 처리 메서드에서 e.Result 형태로 꺼내볼 수 있다.
📌 3. Background Worker를 이용한 WPF 멀티쓰레드 프로그래밍 실습
- 숫자를 입력하면 Background Worker를 통해 ProgressBar에 진행 사항을 표시하고 list Box 에 짝수들을 출력 및 합을 구해 출력하는 예제
- MainWindow.xaml
<Window x:Class="WpfApp3.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:WpfApp3"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Label Content="숫자를 입력하세요" HorizontalAlignment="Left" Margin="21,21,0,0" VerticalAlignment="Top" FontSize="18"/>
<TextBox x:Name="txtNumber" HorizontalAlignment="Left" Height="23" Margin="220,25,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="120" FontSize="18"/>
<Button x:Name="btnStart" Content="시작" HorizontalAlignment="Left" Margin="355,21,0,0" VerticalAlignment="Top" Width="75" RenderTransformOrigin="1.506,-3.218" FontSize="18"/>
<Grid>
<Label Content="숫자를 입력하세요" HorizontalAlignment="Left" Margin="21,21,0,0" VerticalAlignment="Top" FontSize="18"/>
<ProgressBar x:Name="progressBar" HorizontalAlignment="Left" Height="11" VerticalAlignment="Top" Width="497" Margin="21,78,0,0"/>
<Label Content="합계" HorizontalAlignment="Left" Margin="266,108,0,0" VerticalAlignment="Top" FontSize="18"/>
<Label x:Name="lblSum" Content="" HorizontalAlignment="Left" Margin="353,108,0,0" VerticalAlignment="Top" FontSize="18" Width="121"/>
</Grid>
<Button x:Name="btnCancel" Content="중지" HorizontalAlignment="Left" Margin="444,20,0,0" VerticalAlignment="Top" Width="75" RenderTransformOrigin="1.506,-3.218" FontSize="18"/>
<ListBox HorizontalAlignment="Left" Height="242" VerticalAlignment="Top" Width="189" Margin="21,107,0,0"/>
</Grid>
</Window>
- MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace WpfApp3
{
/// <summary>
/// MainWindow.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MainWindow : Window
{
//백그라운드 워커 선언
private BackgroundWorker myThread;
//짝수의 합을 저장할 인스턴스 변수
int sum = 0;
public MainWindow()
{
InitializeComponent();
}
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
//백그라운드 워커 초기화
myThread = new BackgroundWorker()
{
//작업의 진행율이 바뀔 때 ProgressCheanged 이벤트 발생 여부
WorkerReportsProgress = true,
//작업취소 가능 여부 true로 설정
WorkerSupportsCancellation = true
};
//콜백 메서드 정의
//해야할 작업을 실행할 메서드 정의
myThread.DoWork += myThread_DoWork;
//UI쪽에 진행상황을 보여주기 위해 WorkerReportsProgress 속성값이 true 일때만 이벤트 발생
myThread.ProgressChanged += myThread_Progresschanged;
//작업이 완료 되었을 때 실행 할 콜백 메서드 정의
myThread.RunWorkerCompleted += myThread_RunWorkerCompleted;
MessageBox.Show("Worker 초기화");
}
// BackgroundWorker가 실행하는 작업
// DoWork 이벤트 처리 메서드에서 IstNumber.Items.Add(i)와 같은 코드를
// 직접 실행시키면 [ InvalidOperationException ] 오류 발생
private void myThread_DoWork(object sender, DoWorkEventArgs e)
{
int count = (int)e.Argument;
for (int i=1; i<= count; i++)
{
//취소가 눌러졌는지 점검
if (myThread.CancellationPending)
{
e.Cancel = true;
return;
} else
{
//잠시 쉰다.
Thread.Sleep(100);
//DoWork에서 UI로 접근 할 수 없기 떄문에 Dispatcher를 통해 접근
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, //우선순위
(ThreadStart)delegate () //콜백메서드
{
//짝수만 담는다.
if (i % 2 == 0)
{
sum += i;
e.Result = sum;
lstNumber.Items.Add(i);
}
}
);
myThread.ReportProgress(i);
}
}
}
// 작업의 진행률이 바뀔 때 발생
// ProgressBar에 변경사항 출력
// 대체로 현재 진행상태를 보여주는 코드를 여기에 작성
private void myThread_Progresschanged(object sender, ProgressChangedEventArgs e)
{
progressBar.Value = e.ProgressPercentage;
}
//작업완료시
private void myThread_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled) MessageBox.Show("작업 취소");
else if (e.Error != null) MessageBox.Show("에러발생 " + e.Error);
else
{
lblSum.Content = ((int)e.Result).ToString();
MessageBox.Show("작업완료");
}
}
//시작버튼 눌렀을 때
private void BtnStart_Click(object sender, RoutedEventArgs e)
{
int num;
if(!int.TryParse(txtNumber.Text, out num))
{
MessageBox.Show("숫자를 입력하세요");
return;
}
progressBar.Maximum = num;
lstNumber.Items.Clear();
myThread.RunWorkerAsync(num);
}
//취소버튼 눌렀을 때
private void BtnCancel_Click(object sender, RoutedEventArgs e)
{
myThread.CancelAsync();
}
}
}