본문 바로가기
Study/C#

C# Windows Forms 강의 25편: 크로스 스레드 작업과 InvokeRequired

by wawManager 2025. 2. 28.
728x90

1. 강의 개요

이번 강의에서는 Windows Forms 애플리케이션에서 **크로스 스레드 작업(Cross-Thread Operation)**과 InvokeRequired를 활용해 UI 업데이트를 안전하게 처리하는 방법을 학습합니다.
Windows Forms에서는 UI 컨트롤을 메인 스레드(UI 스레드)에서만 업데이트할 수 있으므로, 백그라운드 작업 스레드에서 UI를 안전하게 업데이트하려면 추가적인 조치가 필요합니다.


2. 학습 목표

  1. 멀티스레딩 환경에서 UI 컨트롤을 안전하게 업데이트.
  2. InvokeRequired와 Invoke() 메서드를 사용해 크로스 스레드 작업 처리.
  3. 백그라운드 작업 중 ProgressBar와 Label 업데이트 구현.

3. 크로스 스레드 작업이란?

크로스 스레드 작업은 메인 스레드(UI 스레드) 외의 다른 스레드에서 UI 컨트롤을 직접 수정하려고 할 때 발생합니다.

  • Windows Forms에서는 **스레드 간격 위반(Thread Affinity Violation)**을 방지하기 위해 UI 컨트롤을 직접 업데이트할 수 없습니다.
  • 이 문제를 해결하려면 UI 작업은 항상 메인 스레드에서 실행해야 합니다.

InvokeRequired와 Invoke 메서드

개념 설명 예제

InvokeRequired 현재 스레드가 UI 스레드인지 확인 if (control.InvokeRequired) {...}
Invoke() UI 스레드에서 작업 실행 control.Invoke(new Action(() => {...}));

4. 실습: 백그라운드 작업 중 UI 업데이트

요구사항

  1. Thread 또는 Task로 긴 작업을 백그라운드에서 실행.
  2. 작업 진행 상황을 ProgressBar와 Label로 표시.
  3. Invoke를 사용해 UI를 안전하게 업데이트.

폼 구성

컨트롤 타입 이름 텍스트 위치 크기

ProgressBar progressBar1 (없음) 상단 (400 x 30)
Label lblStatus "상태: 대기 중" 중간 (400 x 30)
Button btnStartWork "작업 시작" 하단 (150 x 30)

코드 작성

Form1.cs

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

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        // 작업 시작 버튼 클릭 이벤트
        private void BtnStartWork_Click(object sender, EventArgs e)
        {
            progressBar1.Value = 0;
            lblStatus.Text = "상태: 작업 진행 중...";

            // Task를 사용한 비동기 작업
            Task.Run(() =>
            {
                for (int i = 1; i <= 100; i++)
                {
                    Thread.Sleep(50); // 50ms 지연
                    UpdateUI(i); // UI 업데이트
                }

                UpdateStatus("상태: 작업 완료");
            });
        }

        // UI 업데이트 (스레드 안전)
        private void UpdateUI(int progress)
        {
            if (progressBar1.InvokeRequired)
            {
                progressBar1.Invoke(new Action(() =>
                {
                    progressBar1.Value = progress;
                }));
            }
            else
            {
                progressBar1.Value = progress;
            }
        }

        // 상태 업데이트 (스레드 안전)
        private void UpdateStatus(string status)
        {
            if (lblStatus.InvokeRequired)
            {
                lblStatus.Invoke(new Action(() =>
                {
                    lblStatus.Text = status;
                }));
            }
            else
            {
                lblStatus.Text = status;
            }
        }
    }
}

Form1.Designer.cs

namespace WindowsFormsApp1
{
    partial class Form1
    {
        private System.ComponentModel.IContainer components = null;
        private ProgressBar progressBar1;
        private Label lblStatus;
        private Button btnStartWork;

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        private void InitializeComponent()
        {
            this.progressBar1 = new ProgressBar();
            this.lblStatus = new Label();
            this.btnStartWork = new Button();
            this.SuspendLayout();

            // progressBar1
            this.progressBar1.Location = new System.Drawing.Point(20, 20);
            this.progressBar1.Name = "progressBar1";
            this.progressBar1.Size = new System.Drawing.Size(400, 30);
            this.progressBar1.TabIndex = 0;

            // lblStatus
            this.lblStatus.AutoSize = true;
            this.lblStatus.Location = new System.Drawing.Point(20, 60);
            this.lblStatus.Name = "lblStatus";
            this.lblStatus.Size = new System.Drawing.Size(120, 20);
            this.lblStatus.TabIndex = 1;
            this.lblStatus.Text = "상태: 대기 중";

            // btnStartWork
            this.btnStartWork.Location = new System.Drawing.Point(20, 100);
            this.btnStartWork.Name = "btnStartWork";
            this.btnStartWork.Size = new System.Drawing.Size(150, 30);
            this.btnStartWork.TabIndex = 2;
            this.btnStartWork.Text = "작업 시작";
            this.btnStartWork.UseVisualStyleBackColor = true;
            this.btnStartWork.Click += new System.EventHandler(this.BtnStartWork_Click);

            // Form1
            this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(450, 200);
            this.Controls.Add(this.btnStartWork);
            this.Controls.Add(this.lblStatus);
            this.Controls.Add(this.progressBar1);
            this.Name = "Form1";
            this.Text = "크로스 스레드 작업 예제";
            this.ResumeLayout(false);
            this.PerformLayout();
        }
    }
}

5. 실행 결과

  1. 작업 시작
    • "작업 시작" 버튼 클릭 시 ProgressBar가 점진적으로 업데이트됩니다.
    • Label에는 작업 진행 상태가 표시됩니다.
  2. UI 멈춤 없이 업데이트
    • 작업이 진행되는 동안 버튼을 클릭하거나 폼을 이동해도 UI는 멈추지 않습니다.
  3. 작업 완료
    • ProgressBar가 100%에 도달하면 Label 상태가 "작업 완료"로 변경됩니다.

6. 주요 개념 요약

  1. 크로스 스레드 작업 방지
    • UI 컨트롤은 메인(UI) 스레드에서만 업데이트할 수 있습니다.
  2. InvokeRequired
    • 현재 스레드가 UI 스레드인지 확인하고, 필요한 경우 Invoke를 사용해 작업을 메인 스레드에서 실행합니다.
  3. Task와 UI 업데이트
    • Task 또는 Thread에서 백그라운드 작업을 실행하며, UI 업데이트는 반드시 Invoke로 처리합니다.

 

728x90