C# 메모. 크로스 스레드 문제
윈폼에서 개발하다 보면 아래와 같은 오류에 부딪히게 될 때가 있다.
이러한 크로스 스레드(Cross-thread) 문제를 해결하는 방법은 주로 아래와 같이 3가지 방법이 있다.
- CheckForIllegalCrossThreadCalls를 통해 해결
- System.Windows.Forms.Control.Invoke를 통해 해결
- System.ComponentModel.BackgroundWorker를 통해 해결
CheckForIllegalCrossThreadCalls
가장 간단한 해결 방법이다. 아래와 같이 코드 한 줄만 작성하면, UI 스레드 외의 스레드를 통해 컨트롤의 Handle에 접근하는 경우를 잡아내지 않도록 만들 수 있다.
CheckForIllegalCrossThreadCalls = false;
그러나 이 방법은 권장되지 않는 방법이다. 다수의 스레드가 특정 컨트롤의 메서드나 속성 중 하나에 접근했을 때 예기치 못한 결과와 부딪히게 될 수도 있다. 프로그램이 갑작스럽게 비정상적으로 종료될 수 있음을 알고서도 이를 회피하는 것과 같다.
System.Windows.Forms.Control.Invoke
버튼을 누르면 스레드 thread2를 통해 Invoke()를 이용한 안전한 방법으로 textBox1.Text를 수정하는 예제다.
public class InvokeThreadSafeForm : Form
{
private delegate void SafeCallDelegate(string text);
private Thread thread2 = null;
private void Button1_Click(object sender, EventArgs e)
{
thread2 = new Thread(new ThreadStart(SetText));
thread2.Start();
Thread.Sleep(1000);
}
private void WriteTextSafe(string text)
{
if (textBox1.InvokeRequired)
{
var d = new SafeCallDelegate(WriteTextSafe);
Invoke(d, new object[] { text });
}
else
{
textBox1.Text = text;
}
}
private void SetText()
{
WriteTextSafe("This text was set safely.");
}
}
우선 아래의 부분을 먼저 살펴보자.
if (textBox1.InvokeRequired)
스레드 thread2가 직접적으로 textBox1.Text의 내용을 수정할 수는 없으므로, UI 스레드를 경유하여 수정해야 할 것이다. 이 InvokeRequired 속성은 현재 스레드와 컨트롤이 만들어진 스레드가 동일하지 않으므로 Invoke()를 사용해야 되는지를 알려준다. UI 스레드의 경우에는 Invoke()를 통해 textBox1.Text를 수정할 필요 없이, 곧바로 textBox1.Text에 접근할 수 있으므로 바로 수정하면 된다.
var d = new SafeCallDelegate(WriteTextSafe);
Invoke(d, new object[] { text })
윈도우 GUI 프로그래밍의 기본 규칙 중 하나는 '컨트롤을 만든 스레드만 컨트롤의 내용에 접근하거나 수정할 수 있다.' 이다. 따라서, 우리가 다른 스레드를 통해 컨트롤에 접근해야 하는 경우 Invoke() 메서드를 통해 UI 스레드에게 "시간이 좀 날때 이 일좀 처리해줘!"라고 메시지를 보낼 수 있다. 실제로 UI 스레드는 자신에게 보내는 모든 메시지를 계속해서 처리할 수 있도록 메시지 펌프(message pump)를 가지고 있다. 즉, 우리가 넘겨준 델리게이트가 컨트롤 textBox1를 만든 UI 스레드에서 실행된다는 얘기다.
하지만 이렇게 한 번 밖에 사용하지 않는데도 델리게이트를 매번 정의하는 것도 귀찮으면, 반환형이 void고 매개변수를 사용하지 않는 간단한 함수에 한해서 아래와 같이 MethodInvoker 델리게이트를 통해 간단하게 만들 수 있다.
private void WriteTextSafe(string text)
{
if (textBox1.InvokeRequired)
{
Invoke((MethodInvoker) delegate() {
WriteTextSafe(text);
});
}
else
{
textBox1.Text = text;
}
}
System.ComponentModel.BackgroundWorker
BackgroundWorker는 복잡한 계산이나 파일 검색, 용량이 큰 파일을 처리하는 등과 같이 시간이 오래 걸리는 작업을 비동기적으로 실행하기 위한 것이다. 먼저, BackgroundWorker를 통해 안전한 방법으로 textBox1.Text를 수정하는 예제를 보도록 하자.
public partial class BackgroundWorkerForm : Form
{
private BackgroundWorker backgroundWorker1;
public BackgroundWorkerForm()
{
InitializeComponent();
backgroundWorker1 = new BackgroundWorker();
backgroundWorker1.DoWork += new DoWorkEventHandler(BackgroundWorker1_DoWork);
backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
}
private void BackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
// 시간이 오래 걸리는 상황을 흉내내기 위해 2초간 대기한다.
Thread.Sleep(2000);
e.Result = "This text was set safely by BackgroundWorker.";
}
private void BackgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
textBox1.Text = e.Result.ToString();
}
private void button1_Click(object sender, EventArgs e)
{
backgroundWorker1.RunWorkerAsync();
}
}
우선 DoWork 이벤트는 메서드 RunWorkerAsync()가 호출될 때 발생하며, BackgroundWorker1_DoWork에서 실제 작업이 이루어진다. 이벤트 핸들러인 BackgroundWorker1_DoWork는 UI 스레드가 아니라 별도의 스레드에서 실행되고, 작업이 모두 끝나면 발생하는 RunWorkerCompleted 이벤트는 UI 스레드에서 처리된다. 이벤트 핸들러로 넘어가는 DoWorkEventArgs의 인스턴스가 어떤 녀석인지에 대해 알아볼 필요가 있다.
- DoWorkEventArgs의 인스턴스는 Argument와 Result라는 속성을 가지고 있다.
- e.Argument: RunWorkerAsync()를 통해 작업을 시작할 때 전달하는 인수다. 예를 들어, BackgroundWorker1.RunWorkerAsync(argument: value)와 같이 값을 넘겨주면, int value = (int)e.Argument; 이런 식으로 다시 참조할 수 있다.
- e.Result: 작업을 모두 마치고 최종적으로 계산된 결과를 다시 메인 스레드로 넘기기 위해 사용할 수 있다. 위의 예제를 보면 쉽게 이해가 갈 것이다.
BackgroundWorker에 대해 더 자세히 알고싶은 경우에는 MSDN에서 확인할 수 있다.