본문 바로가기

개인공부/WPF

[WPF] 멀티쓰레드(Multi Thread)

■ 멀티쓰레드(Multi Thread)란?

여러개의 쓰레드가 동시에 실행하는 것

WPF가 실행되면 Rendering Thread, UI Thread가 생성됩니다.

Rendering Thread는 백그라운드에서 실행되며 WPF화면을 Rendering하는데 사용됩니다.

UI Thread는 사용자의 입력, 이벤트 처리, 화면 그리기 등에 사용됩니다.

WPF는 STA(Single Thread Apartment) 모델

 

■ BackgroundWorker

 - BackgroundWorker 클래스는 UI Thread 외에 시간이 많이 걸리는 작업을 비동기 적으로 작업하기 위한 클래스입니다.

 - BackgroundWorker는 DoWork, ProgressChanged, RunWorkerCompleted 이벤트를 통해 작업 내용을 구현합니다.

  · DoWork : 실제 작업할 내용

  · ProgressChanged : 진행 사항 전달

  · RunWorkerCompleted : DoWork에 구현된 작업이 완료 된 후 실행할 작업

 

■ Dispatcher

 - Dispatcher는 UI Thread 내의 작업 항목 큐를 우선 순위에 따라 유지 관리합니다.

 - Background Thread는 UI Thread에 연결된 객체의 값을 변경하거나 조작하기 위해서는 Dispatcher를 사용해야합니다.

 - Dispatcher를 사용하지 않고 UI 객체를 조작하게 되면 "System.InvalidOperationException"이 발생합니다.

Visual Studio에서 발생된 "System.InvalidOperationException"

 

■ Multi Thread Example (BackgroundWorker, Dispatcher)

 - Example 설명

   · 1부터 입력한 숫자만큼 1씩 증가

   · 증가하면서 listbox, progressbar, label에 진행사항 표기

 

 - MainWindow.xaml

MainWindow.xaml 디자인

<Window x:Class="MultiThread.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:MultiThread"
        mc:Ignorable="d"
        Title="MainWindow" Height="500" Width="500">
    <StackPanel>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
            <Label Content="숫자 입력 : " Margin="10,10,10,10"/>
            <TextBox x:Name="txtBox" Width="100" Margin="10,10,10,10"/>
            <Button x:Name="startBtn" Content="시작" Width="100" Margin="10,10,10,10" Click="startBtn_Click"/>
            <Button x:Name="stopBtn" Content="정지" Width="100" Margin="10,10,10,10" Click="stopBtn_Click"/>
        </StackPanel>
        <ProgressBar x:Name="progressBar" Height="20" Margin="10,10,10,10"/>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
            <Label x:Name="lblNum" Content="0 %" Margin="10,10,10,10"/>            
        </StackPanel>
        <ListBox x:Name="lstBox" Width="400" Height="300" HorizontalAlignment="Center"/>
    </StackPanel>
</Window>

   

 

 - MainWindow.xaml.cs

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

namespace MultiThread
{
    public partial class MainWindow : Window
    {
        private BackgroundWorker bgThread;

        public MainWindow()
        {
            InitializeComponent();
        }

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);

            bgThread = new BackgroundWorker()
            {
                WorkerReportsProgress = true,       // ProgressChanged 이벤트 발생 여부(작업 진행율)
                WorkerSupportsCancellation = true   // 작업 취소 가능 여부
            };

            bgThread.DoWork += Thread_DoWork;       // 작업 method 정의
            bgThread.ProgressChanged += Thread_ProgressChanged;     
            // UI 진행사항, WorkerReportsProgress가 true일 때만 발생
            
            bgThread.RunWorkerCompleted += Thread_RunWorkerCompleted;   // 작업 완료되었을 때 callback method 정의
        }

        // background worker 실행할 작업
        // DoWork 이벤트 메소드에서 UI에 직접 접근하게 되면 "InvalidOperationException" 오류 발생
        private void Thread_DoWork(object sender, DoWorkEventArgs e)
        {
            int count = (int)e.Argument;

            for(int i = 1; i <= count; i++)
            {
                if (bgThread.CancellationPending)
                {
                    e.Cancel = true;
                    return;
                }
                else
                {
                    this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart)delegate ()
                     {
                         e.Result = count;
                         int per = (int)Math.Round((double)(100 * i) / count);
                         lblNum.Content = per + " %";
                         lstBox.Items.Add("진행중 : " + i);
                         lstBox.Items.MoveCurrentToLast();
                         lstBox.ScrollIntoView(lstBox.Items.CurrentItem);
                     });

                    bgThread.ReportProgress(i);
                    Thread.Sleep(100);
                }
            }
        }

        // 작업 진행률
        private void Thread_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            progressBar.Value = e.ProgressPercentage;
        }

        // 작업 완료
        private void Thread_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Cancelled) MessageBox.Show("작업 취소");
            else if (e.Error != null) MessageBox.Show("ERROR 발생" + e.Error);
            else
            {
                progressBar.Value = (int)e.Result;

                lstBox.Items.Add("작업 완료");
                lstBox.Items.MoveCurrentToLast();
                lstBox.ScrollIntoView(lstBox.Items.CurrentItem);
                
                MessageBox.Show("작업 완료");
            }
        }

        private void startBtn_Click(object sender, RoutedEventArgs e)
        {
            int num;

            if(!int.TryParse(txtBox.Text, out num))
            {
                MessageBox.Show("숫자를 입력하세요.");
                return;
            }

            progressBar.Maximum = num;
            lstBox.Items.Clear();
            bgThread.RunWorkerAsync(num);
        }

        private void stopBtn_Click(object sender, RoutedEventArgs e)
        {
            bgThread.CancelAsync();
        }
    }
}

 

 

 - 숫자 30 입력, 진행 완료 전에 정지

 - 숫자 20 입력, 진행 정상 완료