[C#] 60. 윈도우 폼(Window form)에서 스레드(Thread)를 사용하는 방법, 크로스 스레드 문제 해결


Study/C#  2021. 11. 4. 19:12

안녕하세요. 명월입니다.


이 글은 C#의 윈도우 폼(Window form)에서 스레드(Thread)를 사용하는 방법, 크로스 스레드 문제 해결에 대한 글입니다.


이전 글에서 C#에서의 스레드를 설명한 적이 있습니다.

링크 - [C#] 36. 스레드(Thread)를 사용하는 방법, Thread.Sleep 함수 사용법


스레드는 프로그램 내에서 병렬 처리를 뜻합니다. 먼저 윈도우는 프로그램에서 싱글 스레드의 무한 루프로 움직이고 있습니다.

그런데 우리가 버튼의 클릭 이벤트에서 루프를 실행하는 로직을 만들어 보겠습니다.

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

namespace WindowsFormsApp
{
  // Form 클래스를 상속
  public partial class Form1 : Form
  {
    // 맴버 변수 버튼
    private Button button1 = null;
    // 컨트럴 클래스의 초기 설정 함수
    private T setInitControl<T>(T ctl, string name, int point) where T : Control
    {
      // 위치 설정
      ctl.Location = new System.Drawing.Point(27, point);
      // 컨트럴 이름 설정
      ctl.Name = name;
      // 컨트럴 크기 설정
      ctl.Size = new System.Drawing.Size(75, 23);
      // 탭 Index 설정
      ctl.TabIndex = 0;
      // 리턴
      return ctl;
    }
    // 생성자
    public Form1()
    {
      // 초기화
      InitializeComponent();
      // 버튼 인스턴스 생성 후 초기 설정
      this.button1 = setInitControl(new Button(), "Button1", 40);
      // 버튼의 Text 설정
      this.button1.Text = "Button";
      // 폼에 Control 추가
      this.Controls.Add(button1);
      // 이벤트 추가(람다식으로 추가)
      this.button1.Click += (sender, e) =>
      {
        // 0부터 9999까지 루프
        for (int i = 0; i < 10000; i++)
        {
          // 콘솔 출력
          Console.WriteLine(i);
          // 스레드 대기 1초
          Thread.Sleep(1000);
        }
      };
    }
  }
}

프로그램을 실행하고 버튼을 누르면 루프가 끝날 때까지 윈도우 폼은 움직이지 않습니다. 시간이 지나면 응답 없음으로 변할 때도 있습니다.

즉, 윈도우는 싱글 스레드 상태라서 저 함수 스택에 걸리면 처리가 끝날 때까지 움직이지 않습니다.


그렇다면 버튼을 누를 때, 복잡한 처리를 한다고 하면 어떻게 처리를 해야 할까요? 스레드를 사용하면 됩니다.

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

namespace WindowsFormsApp
{
  // Form 클래스를 상속
  public partial class Form1 : Form
  {
    // 맴버 변수 버튼
    private Button button1 = null;
    // 컨트럴 클래스의 초기 설정 함수
    private T setInitControl<T>(T ctl, string name, int point) where T : Control
    {
      // 위치 설정
      ctl.Location = new System.Drawing.Point(27, point);
      // 컨트럴 이름 설정
      ctl.Name = name;
      // 컨트럴 크기 설정
      ctl.Size = new System.Drawing.Size(75, 23);
      // 탭 Index 설정
      ctl.TabIndex = 0;
      // 리턴
      return ctl;
    }
    // 생성자
    public Form1()
    {
      // 초기화
      InitializeComponent();
      // 버튼 인스턴스 생성 후 초기 설정
      this.button1 = setInitControl(new Button(), "Button1", 40);
      // 버튼의 Text 설정
      this.button1.Text = "Button";
      // 폼에 Control 추가
      this.Controls.Add(button1);
      // 이벤트 추가(람다식으로 추가)
      this.button1.Click += (sender, e) =>
      {
        // 스레드 풀 생성
        ThreadPool.QueueUserWorkItem((_) =>
        {
          // 0부터 9999까지 루프
          for (int i = 0; i < 10000; i++)
          {
            // 콘솔 출력
            Console.WriteLine(i);
            // 스레드 대기 1초
            Thread.Sleep(1000);
          }
        });
      };
    }
  }
}

버튼을 클릭을 해서 콘솔에 1씩 출력이 되도 윈도우가 멈추지 않는 것을 확인 할 수 있습니다.

그럼 이번에는 콘솔에 값을 출력하는 것이 아니고 윈도우 컨트럴에서 값이 출력이 되게 만들어 보겠습니다.

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

namespace WindowsFormsApp
{
  // Form 클래스를 상속
  public partial class Form1 : Form
  {
    // 맴버 변수 버튼
    private Button button1 = null;
    private Label label1 = null;
    // 컨트럴 클래스의 초기 설정 함수
    private T setInitControl<T>(T ctl, string name, int point) where T : Control
    {
      // 위치 설정
      ctl.Location = new System.Drawing.Point(27, point);
      // 컨트럴 이름 설정
      ctl.Name = name;
      // 컨트럴 크기 설정
      ctl.Size = new System.Drawing.Size(75, 23);
      // 탭 Index 설정
      ctl.TabIndex = 0;
      // 리턴
      return ctl;
    }
    // 생성자
    public Form1()
    {
      // 초기화
      InitializeComponent();
      // 버튼 인스턴스 생성 후 초기 설정
      this.button1 = setInitControl(new Button(), "Button1", 80);
      // 라벨 인스턴스 생성 후 초기 설정
      this.label1 = setInitControl(new Label(), "Label1", 40);
      // 버튼의 Text 설정
      this.button1.Text = "Button";
      // 폼에 Control 추가
      this.Controls.Add(button1);
      this.Controls.Add(label1);
      // 이벤트 추가(람다식으로 추가)
      this.button1.Click += (sender, e) =>
      {
        // 스레드 풀 생성
        ThreadPool.QueueUserWorkItem((_) =>
        {
          // 0부터 9999까지 루프
          for (int i = 0; i < 10000; i++)
          {
            // Label의 텍스트 설정
            this.label1.Text = $"{i}";
            // 스레드 대기 1초
            Thread.Sleep(1000);
          }
        });
      };
    }
  }
}

버튼을 클릭하면 바로 에러가 발생합니다.

이유는 Window에서 도는 스레드와 스레드 풀에서 도는 스레드가 동기화가 되지 않아서 그렇습니다.

스레드끼리 동기화라고 하면 서로 간에 lock을 설정해서 동기화를 시키면 되는 데, 윈도우 메시지를 돌리고 있는 스레드에 lock 걸 방법이 없습니다.

이것을 C# 윈도우 개발에서는 크로스 스레드 문제라고 합니다.


이걸 할 수 있는 방법이 각 컨트럴에 있는 Invoke 함수를 사용해서 visitor 패턴, 즉 일명 콜백 함수로 이걸 처리할 수 있습니다.

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

namespace WindowsFormsApp
{
  // 확장 함수를 위한 클래스
  public static class Util
  {
    // 확장 함수
    public static void InvokeControl(this Control ctl, Action func)
    {
      // Thread ID 비교
      if (ctl.InvokeRequired)
      {
        // 대리자로 실행
        ctl.Invoke(func);
      }
      else
      {
        // Thread ID가 같으면 그냥 실행(같은 스레드라는 의미)
        func();
      }
    }
  }
  // Form 클래스를 상속
  public partial class Form1 : Form
  {
    // 맴버 변수 버튼
    private Button button1 = null;
    private Label label1 = null;
    // 컨트럴 클래스의 초기 설정 함수
    private T setInitControl<T>(T ctl, string name, int point) where T : Control
    {
      // 위치 설정
      ctl.Location = new System.Drawing.Point(27, point);
      // 컨트럴 이름 설정
      ctl.Name = name;
      // 컨트럴 크기 설정
      ctl.Size = new System.Drawing.Size(75, 23);
      // 탭 Index 설정
      ctl.TabIndex = 0;
      // 리턴
      return ctl;
    }
    // 생성자
    public Form1()
    {
      // 초기화
      InitializeComponent();
      // 버튼 인스턴스 생성 후 초기 설정
      this.button1 = setInitControl(new Button(), "Button1", 80);
      this.label1 = setInitControl(new Label(), "Label1", 40);
      // 버튼의 Text 설정
      this.button1.Text = "Button";
      // 폼에 Control 추가
      this.Controls.Add(button1);
      this.Controls.Add(label1);
      // 이벤트 추가(람다식으로 추가)
      this.button1.Click += (sender, e) =>
      {
        // 스레드 풀 생성
        ThreadPool.QueueUserWorkItem((_) =>
        {
          // 0부터 9999까지 루프
          for (int i = 0; i < 10000; i++)
          {
            // 대리자를 통해 윈도우 스레드에서 아래의 함수를 호출하게 만든다.
            this.label1.InvokeControl(() =>
            {
              // Label의 텍스트 설정
              this.label1.Text = $"{i}";
            });
            // 스레드 대기 1초
            Thread.Sleep(1000);
          }
        });
      };
    }
  }
}

위 static Util 클래스를 만들고 Control 클래스의 확장 함수를 만들었습니다. 그리고 스레드 풀 안에서 label1 인스턴스에 InvokeControl 함수를 호출하여 람다식으로 콜백 함수를 만들었습니다.

버튼을 클릭하면 Label에 숫자가 1초 단위로 바뀌는 것을 확인할 수 있습니다.


여기까지 C#의 윈도우 폼(Window form)에서 스레드(Thread)를 사용하는 방법, 크로스 스레드 문제 해결에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.