프로그래밍에 대해 중급개발자, 초보개발자를 나누는 기준 중 하나가 멀티쓰레드 프로그래밍을 할 수 있는가입니다. 그 만큼 최근 프로그래밍할 때에는 멀티쓰레드가 매우 중요한 요소중 하나입니다. 많은 일을 처리하면서 반응에 즉각적인 반응을 보이는 프로그램을 만들기 위해서는 멀티쓰레드로 만드는 것이 필수 적이죠. 그 뿐만이 아니라 Performance를 유지하기 위해서라도 멀티쓰레드 프로그래밍은 필수입니다. WPF에서 멀티쓰레드 프로그래밍을 하는 방법에 대해 알아보겠습니다.

아시겠지만, C나 C++에서 쓰레드를 만드려면 굉장히 복잡한 과정이 필요합니다. 하지만 Java에서는 굉장히 쉬운 방법으로 멀티쓰레드 프로그래밍을 할 수 있게 되었죠. 쓰레드를 만드는 방법이나 동기를 유지하는 방법도 비교적 쉽게 작성할 수가 있죠. 하지만 java.util.concurrent 패키지가 제공 및 향상되기 전에는 여전히 고려해야 할 것들이 많은 작업이었습니다.

WPF와 C#에서도 쓰레드 관련 클래스들이 많은데요, 굉장히 좋은 기능을 제공하는 클래스들이 있습니다. BackGroundWorkerThreadPool입니다. 이번 포스팅에서는 이 두 가지 클래스의 사용법에 대해 간단히 알아보고 WPF에서 멀티쓰레드 프로그래밍시 주의할 점 하나에 대해서 짚고 넘어가도록 하겠습니다.

1. BackgroundWorker로 비동기적인 작업을 수행하자!

BackgroundWorker는 비교적 무거운 작업을 비동기적으로 백그라운드에서 처리할때 사용하기 좋은 클래스입니다. 무거운 작업인 경우에 메세지 루프에 영향을 주어서 프로그램의 반응성을 나쁘게 만드는데요, 이럴때 이 클래스를 이용하면 간단히 문제를 해결 할 수 있습니다. 특히 업무의 진행량이나 업무가 완성된 후에 수행해야 할 어떤 일이 있는 경우, 아주 쉽게 구현할 수 있게 해줍니다. 간단한 예제와 함께 설명하겠습니다.

BackgroundWorker _backgroundWorker = new BackgroundWorker();

// BackgroundWorker의 이벤트 처리기
_backgroundWorker.DoWork += _backgroundWorker_DoWork;
_backgroundWorker.RunWorkerCompleted += _backgroundWorker_RunWorkerCompleted;
_backgroundWorker.WorkerReportsProgress = true;
_backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(_backgroundWorker_ProgressChanged);

// BackgroundWorker 실행
// 매개변수를 넣어 실행시키는 것이 가능합니다.
// 매개변수라 다수인경우, 배열을 사용하면 됩니다.
_backgroundWorker.RunWorkerAsync(5000);

여기서 Async는 비동기적으로 수행하라는 뜻입니다. BackgroundWorker를 실행시키기 전에 이벤트 처리기를 등록시켜줘야하는데요, 보시면 아시겠지만 다양한 이벤트를 발생시킵니다. 이름만 보면 어떤 이벤트인지는 대략 짐작하시겠지만 간략히 설명해보겠습니다. DoWork는 실제로 업무를 정의하는 이벤트 처리기 입니다. RunWorkerCompleted는 DoWork에서 정의한 업무가 모두 끝났을 때 발생하는 이벤트이고, ProgressChanged는 업무가 진행됨에 따라 진행률이 바뀌었을때 발생하는 이벤트입니다. 이 이벤트는 WorkerReportsProgress를 true로 설정해 주어야 발생합니다. 이제 각 이벤트 처리기에 대해 알아보도록 하겠습니다.

void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {
    // Do something
    Object argument = e.Argument;
    // BackgroundWorker에서 수행할 일을 정의합니다.
}

void _backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) {
    // ProgressChanged
    // 진행률에 변화가 있을때 이벤트가 발생합니다.
    // 현재 얼마나 진행했는지 보여주는 코드를
    // 이곳에 작성합니다.
}

// Completed Method
void _backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
    if (e.Cancelled) {
        statusText.Text = "Cancelled";            
    } else if (e.Error != null) {
        statusText.Text = "Exception Thrown";
    } else {
        // Do Something
        // 일이 모두 마쳤을때 수행되어야할
        // 코드를 이곳에 정의합니다.
        statusText.Text = "Completed";
    }
}

주석으로 모두 처리를 했으니 쉽게 알 수 있으실 겁니다. 이정도로 설명드리면 BackGroundWorker의 사용법에 대해선 대충 아시게 되셨을겁니다. 좀 더 자세한 사항은 관련 MSDN을 보시면 더 자세히 살펴보실 수 있습니다.

한가지 주의할 점이 있는데, 이벤트처리기에서 UI객체를 참조하여 그 값을 직접 바꿀 수 없다는 점입니다. 다시 말해 BackGroundWorkerDoWork 이벤트 처리기에서 TextBox.Text="contents" 와 같은 코드를 실행시키면 InvalidOperationException이 발생하면서 프로그램이 다운됩니다. RunWorkerCompleted 이벤트 처리기에선 이점에 대해선 이번 포스팅 마지막에 설명하도록 하겠습니다.

2. ThreadPool로 작업을 비동기적으로 처리하자!

ThreadPool클래스 입니다. 이름만 보고 이 클래스가 어떤 클래스인지 아신다면 어느 정도 멀티쓰레드 프로그래밍에 대해선 어느정도 아시는 분 이실테지요. 굉장히 유명한 패턴인 ThreadPool 패턴을 구현한 클래스입니다. 간단히 이 패턴에 대해 설명드리겠습니다.

ThreadPool 패턴은 멀티쓰레드 프로그램에서 Thread생성의 오버헤드와 시스템 리소스를 효율적으로 사용하게 해줍니다. 프로그램 시작시 Thread를 미리 몇 개정도를 만들어두어 Thread를 이용해야 하는 어떤 업무가 발생했을때 ThreadPool에 있는 쓰레드를 바로 가져다 쓰고 업무가 끝났을때에는 ThreadPool에 Thread를 반납합니다. 만약 ThreadPool에 쉬고 있는 Thread가 없다면 Thread가 반납될 때까지 Blocking되어 기다립니다. 이로서 얻어지는 점은,

  • Thread가 필요할때 이미 만들어진 Thread를 가져다 쓰므로 Thread생성에 필요한 오버헤드를 발생시키지 않아 속도가 빠르다.
  • Thread를 무한정 많이 생성하지 않기 때문에 프로그램이 돌아가고 있는 시스템에 과도하게 부하를 주지 않을 수 있다.

굉장히 유용한 패턴이죠. 간단히 설명을 했지만 이해가 안되시는 분은 꼭 Google과 같은 검색엔진을 이용해서 ThreadPool에 대해 알아보도록 합시다.

ThreadPool은 static으로 이미 만들어진 Thread를 이용하는 클래스입니다. WaitCallback이라는 클래스에 정의된 업무를 ThreadPool에 넣으면 그 업무를 ThreadPool에 있는 Thread에서 처리합니다. 사용법은 다음과 같습니다.

private readonly WaitCallback HardWorkDelegate = new WaitCallback(this.HardWorkCallback);

private void HardWorkCallback(object oArgument) {
    // do something
    // 어떤 업무에 관한 코드를 정의합니다.
}

private void CallWaitCallback() {
    // ThreadPool에 해당 업무를 시킵니다.
    ThreadPool.QueueUserWorkItem(this.HardWorkDelegate);
}

매우 심플하게 구현이 가능합니다. BackGroundWorker에서 설명드렸습니다만, 역시 ThreadPool에서도 UI객체를 컨트롤 하는 것이 불가능 합니다. 이제 이 부분에 대해서 설명드리겠습니다.

3. Thread에서 InvalidOperationException 문제 해결!

일단 UI쓰레드와 비UI쓰레드에 대해서 설명드리겠습니다.

WPF에는 UI쓰레드와 비UI쓰레드가 있습니다. UI쓰레드는 UI객체를 수정 및 변경할 수 있는 쓰레드입니다. 비UI쓰레드는 UI객체를 변경하려고 시도하면 InvalidOperationException이 발생합니다. 제대로 처리가 안되죠. WPF 어플리케이션의 UI쓰레드는 오직 메인 쓰레드 하나입니다. 따라서, 위의 방법으로 백그라운드에서 돌아가는 쓰레드의 경우 UI객체를 수정 및 변경할 수가 없습니다.

  • WPF에는 UI Thread와 Non-UI Thread가 있다.
  • UI Thread에서만 UI객체를 수정 및 변경할 수 있다.
  • UI Thread는 Main Thraed 단 하나이다.
  • 백그라운드 Thread는 UI객체를 수정 빛 변경할 수 없다.

하지만 꼭 변경해야 하는 상황이 발생합니다. 이런 경우에는 비UI쓰레드에서 UI쓰레드로 UI객체를 변경하라고 알려줘야 합니다. 이 업무를 수행할 수 있도록 해주는 클래스가 바로 Dispatcher입니다. 간단한 예제를 통해 사용법을 알아보도록 하겠습니다.

private delegate void AddTextDelegate(Panel p,(); String text);

private void AddText(Panel p, String text)
{
    p.Children.Clear();
    p.Children.Add(new TextBlock { Text = text });
}

private void TestBeginInvokeWithParameters(Panel p)
{
    if (p.Dispatcher.CheckAccess())
        AddText(p, "Added directly.");
    else
        p.Dispatcher.BeginInvoke(new AddTextDelegate(AddText), p, "Added by Dispatcher.");

}

위 예제는 MSDN에 있는 예제입니다. p.Dispatcher.CheckAccess()를 통해 현재 자신의 쓰레드가 UI객체를 변경할 수 있는 지를 알아낼 수 있습니다. 만약 변경할 수 없다면 p.Dispatcher.BeginInvoke() 함수를 통해 UI객체를 수정 및 변경합니다. 불편합니다만, 이런 방법으로 비UI쓰레드에서 UI객체를 수정 및 변경 할 수 있습니다. WPF 아키텍쳐 때문에 이런 방식을 채용한 것입니다.

4. WPF에서 멀티 쓰레드 프로그래밍을 하자!

위 에서 다양한 방법으로 WPF에서 여러 쓰레드를 이용하는 방법에 알아보았습니다. UI객체와 관련된 이슈사항이 존재하지만 쓰레드를 이용하는 방법이 비교적 쉽다는 것을 알 수 있습니다. 과거 WPF로 프로젝트를 수행할 때, MFC를 다루던 분 께서 저가 멀티쓰레드를 사용하려고 하자 굉장히 힘들거라는 반응을 보였습니다. C++에서는 쓰레드를 만드는 것이 매우 복잡한 일이기 때문이지요. 하지만 java와 C#에서 쓰레드를 사용하는 방법을 알려주자 굉장히 신기하게 생각했습니다. 멀티쓰레드 프로그램으로 더 좋은 퍼포먼스, 반응성 좋은 어플리케이션을 만듭시다!