프로그래밍 관련/C#

C# 메모. 크로스 스레드 문제

LAYER6AI 2019. 4. 9. 16:41

윈폼에서 개발하다 보면 아래와 같은 오류에 부딪히게 될 때가 있다.

 

 

이러한 크로스 스레드(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에서 확인할 수 있다.