2013. 5. 8. 18:10

간만에 WPF를 하는데.....

버튼으로 테스트해 보니 프로그램이 잘 돌아가서 타이머를 하나 만들어서 주기적으로 실행하려고 돌렸는데...

에러가 퐝~

 

연관글 영역

 

 

1. 원인
많은 UI 구성 요소에서 호출 스레드가 필요하므로 해당 스레드는 STA여야 합니다.

혹은
"다른 스레드가 소유하고 있는 오브젝트에 현재 스레드가 접근할 수 없다"
(The calling thread cannot access this object because a different thread owns it.)

 

이런 식의 오류가 나기도 합니다.

 

하도 오랜만에 WPF를 해서 감이 없네요;;

WPF나 실버라이트에서 타이머는 다른 스레드기 때문에 UI스레드에 접근할 때 에러가 납니다.

 

 

2. 해결 방법

이럴 때는 디스패처(Dispatcher) 인보크를 사용하여 작업해야 합니다.

(참고 : MSDN - Dispatcher.Invoke 메서드)

 

간단하게 아래와 같이 디스패처 인보크를 사용할 수 있습니다.

Dispatcher.Invoke(DispatcherPriority.Normal
    , new Action(
        delegate 
        {      
            //사용할 메서드 및 동작
            ScrollGraph_001.AddStep(LastData); 	
        }));

 

참 쉽죠?

 

디스패처는 비동기로 해당 액션을 UI스레드의 대기 줄에 올려둡니다.

비동기 프로그래밍을 하셔야 합니다.

 

만약 잘 모르겠다 싶으면 디스패쳐안의 액션에서 동작하는 개체는 모두 디스패처에게 넣어서 작업하는 것이 좋습니다.

디스패처는 큐기 때문에 여러 개를 인보크(invoke)하면 순서대로 쌓여서 디스패처에 추가된 액션끼리는 동기처럼 동작하게 됩니다.

 

 

3. 자신의 스레드인지 확인

'Dispatcher.CheckAccess()'를 통해 자신의 스레드 여부를 확인할 수 있습니다.

('WinForm'에서의 'InvokeRequired'와 같은 기능입니다.)

if (true == Dispatcher.CheckAccess())
{//내 쓰래드다.
}
else
{//내 쓰래드가 아니다.
}

 

 

4. UI 요소라면 모두 '디스패처(Dispatcher)'를 가지고 있다.

작업하는 위치가 메인 윈도우가 아니라면 직접 디스패처에 접근할 수 없습니다.

 

1) 메인 윈도우를 전달받거나 

2) 사용하려는 UI를 전달받아 

해당 개체의 디스패처속성에 접근하여 사용할 수 있습니다.

if (true == MainUi.Dispatcher.CheckAccess())
{//내 쓰래드다.
    MainUi.Children.Add(newIoUC);
}
else
{//내 쓰래드가 아니다.
    MainUi.Dispatcher.Invoke(DispatcherPriority.Normal
        , new Action(
            delegate
            {
                //사용할 메서드 및 동작
                MainUi.Children.Add(newIoUC);
            }));
}

 

위 코드를 보면 메인UI 요소를 전달받지 않고

일부 UI 요소만 전달받아서 사용하는 경우라도 사용할 수 있음을 알 수 있습니다.

(Panel, Grid, Button 등등)

 

그리고 모든 UI 요소는 결국 디스패처를 통해 UI 스레드에서 처리된다는 것도 알 수 있습니다.

 

 

공통화 함수

아래 함수를 재사용하기 쉽도록 적절한 위치에 선언하여 사용합시다.

/// <summary>
/// 크로스 스레드 체크를 하고 상황에 맞게 처리한다.
/// </summary>
/// <param name="controlThis"></param>
/// <param name="action"></param>
public static void CrossThread_WPF(
    Control controlThis
    , Action action)
{
    if (false == controlThis.Dispatcher.CheckAccess())
    {//다른 쓰래드다.
        controlThis.Dispatcher.Invoke(new Action(
            delegate ()
            {
                action();
            }));
    }
    else
    {//같은 쓰래드다.
        action();
    }
}

 

 

사용할 때는 아래와 같이 사용합니다.

CrossThread_WPF(this
    , () => 
    { 
    	// 필요한 동작
    });

 

 

마무리

테스트 프로젝트 - github - dang-gun/DotNetSamples/CrossThreadTest_WPF

WPF 뿐만 아니라 C#도 UI는 다른 스레드에서 동작합니다.

신경 안 쓰면 가끔 까먹는 내용이죠 ㅎㅎㅎ

 

각 플랫폼 별로 미묘하게 다르니 확인하고 사용하셔야 합니다.