본문 바로가기

Programming/C#/Xna/Kinect/WPF

[C#] Cross Thread 처리방법

에러내용 : "Cross-thread operation not valid: Control 'rtb_console' accessed from a thread other than the thread it was created on."


최근에 소켓프로그램을 개발하던 중에도 Cross Thread 문제를 겪었다.
서버에서 Thread을 생성하여 데이터 수신을 기다리고, 수신된 데이터의 내용을 RichTextBox에
출력하려는 작업중에 
Cross-thread operation not valid: Control 'rtb_console' accessed from a thread other than the thread it was created on. 
이러한 메세지를 내뿜고 뻗어버렸다.

이런 문제를 포스팅하기 위해 샘플소스를 작성하였다.


에러가 발생하는 소스
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace MyServer
{
    public partial class Form1 : Form
    {
        private Thread _thread;
        private RichTextBox rtb_console;

        public Form1()
        {
            InitializeComponent();

            _thread = new Thread(new ThreadStart(ThreadProc));
            _thread.Start();
        }

        private void ThreadProc()
        {
int count = 0;
            while (true)
            {
                SetTextBox(count,"hello");
    count++;
            }
        }
        
        private void SetTextBox(int count, String str)
        {
             
rtb_console.Text = count.ToString() +"번째" + str;
         }
    }
}



프로그램은 기본적으로 하나의 Thread(MainThread라 하자)로 시작되고, 그 Thread 내에서 UI를 화면에 그리는 메세지와 이벤트도 처리하게 된다. 하지만 사용자가 추가로 생성한 Thread는 이와 별개에 작업을 하고 있기 때문에 MainThread에 의해 그려진 UI영역(컨트롤)등을 직접 접근하게 되면 MainThread입장에서는 UI관련 메세지와 이벤트를 처리하고 있는데 다른 Thread가 방해를 하는 셈이다. 이런 상황을 Cross Thread라 한다.

위에 소스를 보게되면 프로그램이 실행되면서 시작하는 Thread에서 Form1 객체를 생성하였고, Form1객체가 RichTextBox컨트롤을 생성하였다. 예외를 발생시키기위해 임의로 Thread하나를 생성하였고, 그 Thread 안에서 RichTextBox컨트롤에 직접 접근하였다.
역시 밑줄 친 라인에서 예외를 발생시킨다.

이문제를 해결하는 방법은 Invoke 메소드를 이용하는 것이다.
MSDN에서는 Invoke를 다음과 같이 설명하고 있다.

WPF에서는 DispatcherObject를 만든 스레드만 해당 개체에 액세스할 수 있습니다. 예를 들어 기본 UI 스레드에서 분리된 백그라운드 스레드는 UI 스레드에서 작성된 Button의 콘텐츠를 업데이트할 수 없습니다. 백그라운드 스레드가 Button의 Content 속성에 액세스하려면 백그라운드 스레드에서 UI 스레드에 연결된 Dispatcher에 작업을 위임해야 합니다. 이 작업은 Invoke 또는 BeginInvoke를 사용하여 수행할 수 있습니다. Invoke는 동기적이고 BeginInvoke는 비동기적입니다. 작업은 지정된 DispatcherPriority에서 Dispatcher의 이벤트 큐에 추가됩니다. 

다른 Thread에서는 UIThread(MainThread)에 접근할 수 없기 때문에 
UIThread의 Invoke메소드를 이용하여,
 다른 Thread에서 호출할 메소드를 delegate로 
정의하여 넘겨줘야 한다. 
호출될 메소드의 파라메터가 있다면 이는 object로 넘겨주면된다.
이렇게 해주게 되면 MainThread에게 너 할일 다하고,
이것도 좀 해줘라는 식으로 전달되게 된다.


수정된 소스
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace MyServer
{
    public partial class Form1 : Form
    {
        private Thread _thread;
        private RichTextBox rtb_console;
        private delegate void SetTextBoxCallback(int count, String str);

        public Form1()
        {
            InitializeComponent();

            _thread = new Thread(new ThreadStart(ThreadProc));
            _thread.Start();
        }

        private void ThreadProc()
        {
            int count = 0;
            while (true)
            {
                SetTextBox(count,"hello");
                count++;
            }
        }

        private void SetTextBox(int count, String str)
        {
            if (this.rtb_console.InvokeRequired)
            {
                SetTextBoxCallback setTextBoxCallback =                                           
                                                                      new  SetTextBoxCallback(SetTextBox);
                this.Invoke(setTextBoxCallback, new object[] { count, str });
            }
            else
            {
                rtb_console.Text = count.ToString() +"번째" + str;
            }
        }
    }
}


private delegate void SetTextBoxCallback(int count, String str);
추가 되었고, SetTextBox에서 InvokeRequired라는 속성을 사용하였다.

InvokeRequired는 다른 Thread로부터 호출되어 Invoke가 필요한 상태를 체크하여 true/false를 리턴한다. 따라서 Invoke가 필요한 상태일때는 Invoke메소드에 의해 호출될 SetTextBox를 delegate로 넘겨주면 된다.

위 소스에서 사실은 this.rtb_console.Invoke를 호출해야 하지만 this.Invoke로 호출하였는데,
this.rtb_console(TextBox)이나 this(Form)나 MainThread의 영역이기 때문에 this.Invoke로도 같은 MainThread에게 
위임하게 된다.