본문 바로가기

.NET/WinForm

[WinForm] 크로스 스레드(Cross Thread) 문제 해결

WinForm에서 Thread를 사용해 폼의 컨트롤을 갱신하려고 시도하면 크로스 스레드 예외가 발생한다.

이는 컨트롤을 생성해 관리하는 스레드는 메인 스레드인데, 사용자가 생성한 외부 스레드에서 메인 스레드의 자원을 수정하려고 시도했기 때문에 발생하는 예외이다.

 

가장 쉬운 해결 방법은 스레드 대신 Timer를 사용하는 것이다. 타이머는 메인 스레드 내에서 돌아가는 비동기 실행기이기 때문에 크로스 스레드 예외가 발생하지 않는다.

그러나 이 방법은 비동기 처리 방법을 타이머 하나로 제한하므로 완벽한 해결 방법이라고 볼 수 없다. 단지 스레드보다 관리하기도 쉽고 크로스 스레드 예외를 발생시키지 않는 한 가지 해결 방법일 뿐이다.

 

크로스 스레드의 정석적인 해결 방법은 Invoke() 메서드를 사용하는 것이다.

Invoke() 메서드는 매개변수로 지정된 대리자를 해당 컨트롤이 속한 스레드로 전달하여 대신 처리해줄 것을 요청하는 메서드로, 외부 스레드에서 Invoke() 메서드를 통해 갱신하고자 하는 컨트롤이 속한 메인 스레드에 일련의 동작을 전달할 수 있다.

 

object Control.Invoke(Delegate)

전달할 동작은 delegate(대리자) 형식이어야 하며, 반환 값이 있는 대리자인 경우 반환 값을 object 형식으로 돌려준다.

 

사용 예시로 현재 시각을 지속적으로 텍스트박스 컨트롤에 집어넣는 코드를 짜 보도록 하자.

using System;			// DateTime을 위해 필요
using System.Threading;		// Thread를 위해 필요
using System.Windows.Forms;

public partial class Form1 : Form
{
	private bool run = false;
    
    public delegate void ChangeTimeHandler();
    public ChangeTimeHandler ChangeTime;
    
	public Form1()
    {
    	InitializeComponent();
        
        ChangeTime += changeTime;
        new Thread(timer).Start();
    }
    
    private void changeTime()
    {
    	textBox1.Text = DateTime.Now.ToString("hh:mm:ss");
    }
    
    private void timer()
    {
    	if(run) return;	// 스레드가 이미 동작중이라면 리턴한다.
        
    	run = true;
        while(run)
        {
        	// ChangeTime에 등록된 메서드를 실행할 것을 요청함
        	Invoke(ChangeTime);
        }
    }
    
    private void button_Start_Click(object sender, EventArgs e)
    {
    	new Thread(timer).Start();
    }
    private void button_Stop_Click(object sender, EventArgs e)
    {
    	run = false;
    }
    private void form1_FormClosing(object sender, FormClosingEventArgs e)
    {
    	// Stop 버튼을 누르지 않고 Form을 종료할 경우를 위해 스레드를 중단시키는 것.
		run = false;
    }
}

 

매개변수가 있는 메서드를 Invoke하려면 Invoke(Delegate method, params object[] args) 형태의 Invoke 메서드를 사용하면 된다.

만일 위 코드에서 매개변수가 있는 메서드를 사용하고자 한다면 아래와 같이 바꿀 수 있겠다.

아래 코드에서는 ChangeTime에 등록될 메서드인 changeTime()에서 DateTime을 매개변수로 받아서 사용하도록 하였다.

using System;			// DateTime을 위해 필요
using System.Threading;		// Thread를 위해 필요
using System.Windows.Forms;

public partial class Form1 : Form
{
	private bool run = false;
    
    public delegate void ChangeTimeHandler(DateTime time);
    public ChangeTimeHandler ChangeTime;
    
	public Form1()
    {
    	InitializeComponent();
        
        ChangeTime += changeTime;
        new Thread(timer).Start();
    }
    
    private void changeTime(DateTime time)
    {
    	textBox1.Text = time.ToString("hh:mm:ss");
    }
    
    private void timer()
    {
    	if(run) return;	// 스레드가 이미 동작중이라면 리턴한다.
        
    	run = true;
        while(run)
        {
        	// ChangeTime에 등록된 메서드를 실행할 것을 요청함
            // 매개변수는 DateTime.Now
        	Invoke(ChangeTime, DateTime.Now);
        }
    }
    
    private void button_Start_Click(object sender, EventArgs e)
    {
    	new Thread(timer).Start();
    }
    private void button_Stop_Click(object sender, EventArgs e)
    {
    	run = false;
    }
    private void form1_FormClosing(object sender, FormClosingEventArgs e)
    {
    	// Stop 버튼을 누르지 않고 Form을 종료할 경우를 위해 스레드를 중단시키는 것.
		run = false;
    }
}

 

주제와는 관련 없지만, 스레드 프로그래밍을 할 때는 폼이 예기치 못한 상황(오류 뿐만 아니라 사용자의 액션에 의해서 등)을 대비하여 프로그래밍하는 것이 중요하다. 위 코드에서는 사용자가 Stop 버튼을 누르지 않고 폼을 닫아버려 스레드가 메모리에 잔류하는 것을 방지하기 위해 FormClosing 이벤트를 등록하였다.

디버그 모드가 아닌 실행 파일 또는 릴리즈 모드에서 실행시 굳이 스레드를 종료하지 않고 폼을 닫아버려도 별다른 이상이 발생하지 않는데, 이것은 폼 종료 시 메인 스레드가 종료되며 Invoke()를 호출할 대상 스레드가 더 이상 남아있지 않기 때문에 내부적으로 InvalidAsynchronousStateException이 발생하며 스레드가 중단되어 그렇게 보이는 것이다.

던져지는 예외가 명확하게 있음에도 받지 않는(못하는) 것은 좋은 방법이 아니다.