제임스딘딘의
Tech & Life

개발자의 기록 노트/C#

[C#] WPF Thread

제임스-딘딘 2012. 2. 28. 18:46

WPF Threading

 

WPF는 새로운 스레드 프로그래밍에 대한 보다 단순화된 모델을 제시한다.

대부분의 응용프로그램은 하나의 스레드만으로 실행 되지는 않는다. WPF 역시 렌더링 스레드(Rendering Thread), 사용자 입출력을 담당하는 UI 스레드(UI Thread) 등 여러개의 스레드가 동시에 실행된다. 

개발자가 여러 개의 스레드를 다룰수록 복잡하고 디버깅 등의 어려움이 따르게 마련이다.

 

Dispatcher 와 DispatcherObject

Dispatcher는 스레드에 포함된 작업큐를 말한다.

작업큐에는 수행해야 할 아이템들이 대기하게 되고 정해진 순서에 따라 수행이 된다. 

당연히 Dispatcher 스레드 간에는 공유 될 수 없고 하나의 스레드에만 속해야 한다.

Dispatcher에는 사용자가 직접 대기를 시키던 혹은 운영체제에 의해 대기가 되던 수행해야 할 작업들이 대기하게 된다.

 

DispatcherObject는 자신이 생성되고 실행되는 스레드의 Dispatcher에 접근이 가능하도록 설계된 객체이다. 

WPF의 많은 객체들이 여기에서 파생 된다.

DispatcherObject는 Dispatcher라는 하나의 속성을 지원하는데 스레드의 Dispatcher에 접근하는 방법이다.

Distatcher에 수행해야 할 작업을 삽입하려면 Invoke 나 BeginInvoke 메소드를 통해 한다. Invoke나 BeginInvoke는 비동기 처리를 의미한다.

 

delegate void Work();

private void button1_Click(object sender, RoutedEventArgs e)

{

Dispatcher.Invoke(DispatcherPriority.Normal,new Work(WorkOne) );

}

private void WorkOne()

{

if (button1.CheckAccess())

{

button1.Content = "Hello";

}

}


 

위 코드는 button1을 클릭 했을 때 this 객체의 Dispatcher 에 WorkOne()을 작업으로 할당하고 있다. 

button1.CheckAccess()는 button1에 Dispatcher가 접근 할 수 있는지를 알아보는 메소드로 bool 값을 반환한다. 

VerifyAcces() 메소드도 지원하는데 접근이 안 될 경우 예외를 발생하는 차이가 있다.

 

DispatcherPriority는 많이 세분화 되었다. 이제 10단계로 나누어진다.

이전 닷넷 버전의 스레드 관리 모델보다 정교하다.

 

  1. Invalid 열거형 : 값이 -1이다. 잘못된 우선 순위이다.

  2. Inactive 열거형 : 값이 0이다. 작업이 처리되지 않는다.

  3. SystemIdle 열거형 : 값이 1이다. 시스템이 유휴 상태일 때 작업이 처리된다.

  4. ApplicationIdle 열거형 : 값이 2이다. 응용 프로그램이 유휴 상태일 때 작업이 처리된다.

  5. ContextIdle 열거형 : 값이 3이다. 백그라운드 작업이 완료된 후 작업이 처리된다.

  6. Background 열거형 : 값이 4이다. 유휴 상태가 아닌 다른 모든 작업이 완료된 후 작업이 처리된다.

  7. Input 열거형 : 값이 5이다. 입력과 동일한 우선 순위로 작업이 처리된다.

  8. Loaded 열거형 : 값이 6이다. 레이아웃과 렌더링이 종료되었지만 입력 우선순위의 항목이 처리되기 전에 작업이 처리된다. 특히 Loaded이벤트를 발생시킬 때 사용된다.

  9. Render 열거형 : 값이 7이다. 렌더링과 동일한 우선 순위로 작업이 처리된다.

  10. DataBind 열거형 : 값이 8이다. 데이터 바인딩과 동일한 우선 순위로 작업이처리된다.

  11. Normal 열거형 : 값이 9이다. 일반 우선 순위로 작업이 처리된다. 일반적인응용 프로그램 우선 순위이다.

  12. Send 열거형 : 값이 10이다. 다른 비동기 작업 전에 작업이 처리된다.가장 높은 우선 순위이다.

 

단일 스레드를 이용한 UI 제어

 

TextBox와 Button 하나를 이용한 시나리오로 버턴은 시작 상태에서 Start라는 Text를 가지고 있다. 

클릭과 동시에 TextBox에는 값이 1 만큼 씩 증가 되어 표시된다. 

그리고 버튼의 Text는 Stop 으로 바뀐다. Stop 버턴을 클릭하면 멈추고 Resume 으로 바뀐다. 

이러한 가운데 TextBox text 는 상황에 맞게 증가와 정지를 반복한다.

 

bool runok = false;

 

delegate void Work();

private void button1_Click(object sender, RoutedEventArgs e)

{

runok = !runok;

if (runok)

{

button1.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, new

Work(Display));

button1.Content = "Stop";

} else {

button1.Content = "Resume";

}

}

public void Display()

{

int k = int.Parse(textBox1.Text);

k++;

textBox1.Text = k.ToString();

if(runok)

button1.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, new Work(Display));

}




 

하나의 스레드를 사용해 위의 시나리오를 구현하는 것은 이전의 스레드 제어 모델을 이용하면 다소 복잡 할 수 있겠지만 예제에서는 Button 의 Dispatcher에 WorkItem을 비동기로 대기시키고 이때 DispatcherPriority를 조정 함으로써 간단히 구현 했다.

 

 

백그라운드 스레드를 이용한 UI 처리


백그라운드 스레드에서 UI 스레드에 접근해서 UI 변경을 시도 할 때 크로스 스레드 문제가 발생하고 응용 프로그램의 실행이 차단되는 경우가 종종 있다.

아래의 코드는 기존의 닷넷에서 Windows UI를 처리하던 패턴과 닮아 있다.

아래의 코드에서 delegate를 비동기로 호출함으로써 백그라운드 스레드를 생성한다. 

UI를 처리하는 스레드와는 다른 스레드가 생성 된다. 버튼은 비활성화 시켜서 사용자의 접근을 차단하고 대기하게 한다.

그러나 버튼 이외의 컨트롤은 활성화가 유지 된 상태이다.


delegate void Work();

delegate void UpdateUI();

private void button1_Click(object sender, RoutedEventArgs e)

{

textBox1.Text = "처리 중입니다";

button1.Content = "처리중";

new Work(Processing).BeginInvoke(null, null);

button1.IsEnabled = false;

}

 

private void Processing()

{

Thread.Sleep(6000);

button1.Dispatcher.BeginInvoke(

System.Windows.Threading.DispatcherPriority.Normal, new

UpdateUI(Display));

}

private void Display()

{

textBox1.Text = "처리 완료";

button1.Content = "처리 완료";

button1.IsEnabled = true;

}


 


아래의 그림에서와 같이 처리중이라는 메시지와 함께 버턴은 비활성화 되어있다.

델리게이트 호출 내에서 button1의 Dispatcher 에 UI 변경 작업을 할당함으로서 크로스 스레드 문제는 해결 되었다.


[출처] WPF Thread|작성자 




니시오카

델리게이트를 비동기 호출을 통한 백그라운드 작업 호출 델리게이트 내에서 Dispatcher 에 접근 해 UI를 갱신하는 패턴은 WPF의 백그라운드 작업의 전형적인 사례가 될 만하다.

 


멀티스레드


앞에서 본 것처럼 하나의 스레드로도 많은 것을 처리 할 수 있지만 여러 개의 스레드로 작업하는 것이 더 편리 할 수도 있다. 

대표적인 시나리오로 웹서핑의 경우 하나의 브라우져 보다 여러 개를 사용하는 것이 편리하다. 또한 하나의 작업이 긴 시간을 점유하더라도 문제가 되지를 않는다.

 

private void NewWindowHandler(object sender, RoutedEventArgs e)

{

Thread newWindowThread = new Thread(new

ThreadStart(ThreadStartingPoint));

newWindowThread.SetApartmentState(ApartmentState.STA);

newWindowThread.IsBackground = true;

newWindowThread.Start();

}

 

private void ThreadStartingPoint()

{

Window1 tempWindow = new Window1();

tempWindow.Show();

System.Windows.Threading.Dispatcher.Run();

}


 

새로운 스레드를 생성하고 콜백을 호출하는 코드는 이전 버전과 같다. 

새로운 스레드 상에서 새로운 Window를 생성하고 호출하는 데 별도의 스레드이므로 독립적인 작업을 수행 할 수 있다.

 

System.Windows.Threading.Dispatcher.Run();

부분은 스레드의 기본 프레임을 실행하기 위함이다.

[출처] WPF Thread|작성자 니시오카컴포넌트 상의 스레드



컴포넌트상의 스레드


컴포넌트는 비주얼을 갖지 않는 구성요소 인데 WPF에서 컴포넌트를 호출 할 때 호출 스레드의 컨텍스트를 어떻게 다룰 것 하는 문제를 해결 해야 한다.

DispatcherSynchronizationContext는 호출 컨텍스트의 Dispatcher의 경량 버전으로 호출 스레드의 Dispatcher를 접근 한다.

 

class MyComponent:Component

{

public void RunASync()

{

DispatcherSynchronizationContext cx = (DispatcherSynchronizationContext)

DispatcherSynchronizationContext.Current;

SendOrPostCallback callback = new SendOrPostCallback(DoEvent);

cx.Post(callback, null);

}

 

private void DoEvent(object o)

{

//Do something...

}

}



Dispatcher에서 Invoke 나 BeginInvoke처럼 Send 와 Post를 제공한다.

Send는 동기를 Post는 비동기를 지원한다. 위의 코드는 비동기 코드의 패턴을 나타내고 있다.

 


Dispatcher 비활성화를 통한 잠금


WPF 이전의 스레드 동기화 방법은 객체에 Lock을 사용하던 방법 ,스레드 직렬화, 이벤트모델,mutex 등을 사용 했다.WPF에서는 Dispatcher의 DisableProcessing를 이용해 다음과 같이 사용 한다.

 

DispatcherProcessingDisabled dpd= Dispatcher.DisableProcessing()

 

// 비 활 성 화 영 역

dpd.Dispose();


 

비활성화의 효과로는 CLR 잠금에서 내부적으로 메시지를 펌프하지 않고 DispatcherFrame 개체 푸시가 허용되지 않고 메시지 처리가 허용되지 않는다.

[출처] WPF Thread|작성자 니시오카

[출처]



 
 WPF Thread|작성자 니시오카