C#/C# WPF 개념

[C# WPF] WPF 멀티 쓰레드, BackgroundWorker

냠냠쿠 2023. 8. 17. 16:36
728x90

 

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();
        }
    }
}

728x90