WPF Threads
발송자를 사용하여 응답 성능이 뛰어난 응용 프로그램 작성
Shawn Wildermuth
이 기사에서 다루는 내용:
|
이 기사에서 사용하는 기술: .NET Framework 3.0, Windows Presentation Foundation |
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
사용하기 쉽고 자연스러우며 아름답기까지 한 인터페이스를 만드는 데 몇 달을 소비했지만 막상 사용자는 인터페이스가 반응하기를 기다리며 지루한 시간을 보내야 한다면 이처럼 딱한 일도 없을 것입니다. 오랫동안 실행되는 프로세스 때문에 응용 프로그램 실행이 중단되는 것을 바라보는 일도 유쾌한 경험은 아닙니다. 그러나 응답 성능이 뛰어난 응용 프로그램을 작성하려면 오랫동안 실행되는 프로세스를 다른 스레드에서 작동하도록 하여 UI 스레드가 사용자의 작업을 지원할 수 있게끔 하는 주의 깊은 계획이 필요합니다.
응답 성능에 대한 필자의 첫경험은 Visual C++®과 MFC를 사용하여 첫 번째 표를 작성하던 시기로 거슬러 올라갑니다. 필자는 복잡한 처방전의 모든 약품을 보여 주는 약국용 응용 프로그램 개발을 돕고 있었습니다. 문제는 약품의 수가 30,000가지나 되었다는 것입니다. 따라서 우리는 처음 한 화면에 해당하는 약품을 UI 스레드에서 처리(약 50밀리초)하고, 보이지 않는 나머지 약품을 백그라운드 스레드를 사용하여 처리(약 10초)하기로 결정했습니다. 이 프로젝트는 성공적이었으며 필자는 사용자가 인지하는 성능이 실제 성능보다 더 중요할 수 있다는 귀중한 교훈을 얻었습니다.
WPF(Windows® Presentation Foundation)는 매력적인 사용자 인터페이스를 만들 수 있는 훌륭한 기술이지만 이를 사용한다고 해서 응용 프로그램의 응답 성능을 고려하지 않아도 된다는 의미는 아닙니다. 오랫동안 실행되는 프로세스의 유형이 데이터베이스에서 대량의 결과를 검색하는 것이든, 비동기 웹 서비스 호출을 수행하는 것이든, 그 밖의 부하가 높은 다른 작업을 수행하는 것이든 관계없이 응용 프로그램의 응답 성능을 높이면 장기적으로 사용자에게 도움이 됩니다. 그러나 WPF 응용 프로그램에서 비동기 프로그래밍 모델 사용을 시작하기 전에 WPF 스레딩 모델을 이해하는 것이 중요합니다. 이 기사에서는 스레딩 모델을 소개하는 데 그치지 않고 발송자 기반 개체의 작동 방식을 설명하고, BackgroundWorker를 사용하여 매력적이고 응답 성능이 뛰어난 사용자 인터페이스를 작성하는 방법을 살펴보겠습니다.
스레딩 모델
모든 WPF 응용 프로그램은 렌더링과 사용자 인터페이스 관리를 위한 두 개의 중요한 스레드로 시작합니다. 렌더링 스레드는 백그라운드에서 실행되는 숨겨진 스레드이므로 일반적으로 여러분이 관여할 스레드는 UI 스레드입니다. WPF에서는 대부분의 개체를 UI 스레드에 연결하도록 요구하고 있습니다. 이를 스레드 선호도라고 하며, WPF 개체를 생성한 스레드에서만 해당 개체를 사용할 수 있음을 의미합니다. 다른 스레드에서 WPF 개체를 사용하면 런타임 예외가 발생합니다. WPF 스레딩 모델은 Win32® 기반 API와 문제 없이 상호 운용됩니다. 이것은 WPF가 모든 HWND 기반 API(Windows Forms, Visual Basic®, MFC 또는 Win32)를 호스팅할 수 있으며, 이러한 API에서 호스팅될 수도 있음을 의미합니다.
스레드 선호도는 WPF 응용 프로그램의 우선 순위 결정 메시지 루프인 Dispatcher 클래스에서 처리됩니다. 일반적으로 WPF 프로젝트에는 모든 사용자 인터페이스 작업에 대한 채널링을 수행하는 발송자 개체가 한 개(따라서 UI 스레드 역시 한 개) 포함됩니다.
일반적인 메시지 루프와는 달리 각각의 작업 항목에는 특정한 우선 순위가 지정되어 발송자를 통해 WPF로 전송됩니다. 이를 통해 우선 순위에 따라 항목의 순서를 지정하거나 시스템이 처리할 수 있을 때까지 특정 유형의 작업을 연기할 수 있습니다. 예를 들어 일부 작업 항목은 시스템이나 응용 프로그램이 유휴 상태가 될 때까지 연기할 수 있습니다. 항목에 우선 순위를 지정할 수 있게 됨에 따라 WPF는 특정 유형의 작업에 더 많은 액세스 부여하고, 다른 작업에 비해 스레드에 더 많은 시간을 부여할 수 있게 되었습니다.
이 기사의 뒷부분에서는 렌더링 엔진을 통해 입력 시스템보다 높은 우선 순위로 사용자 인터페이스를 업데이트할 수 있다는 것을 보여줄 것입니다. 이것은 사용자가 마우스, 키보드 또는 잉크 시스템으로 어떤 작업을 하고 있든지 관계없이 애니메이션으로 사용자 인터페이스 업데이트를 계속할 수 있음을 의미합니다. 이를 통해 사용자 인터페이스의 응답 성능을 높일 수 있습니다. 예를 들어 Windows Media® Player와 비슷한 음악 재생 응용 프로그램을 작성한다고 가정해 보겠습니다. 이 경우 사용자가 인터페이스를 사용하고 있는지 여부에 관계없이 진행률 표시줄 및 기타 정보를 비롯한 음악 재생과 관련된 정보를 표시하는 것이 좋습니다. 이렇게 하면 사용자에게 중요한 작업(이 경우에는 음악 감상)을 처리하는 인터페이스의 응답 성능을 향상시킬 수 있습니다.
발송자의 메시지 루프를 사용하여 작업 항목을 사용자 인터페이스 스레드로 채널링하는 것 이외에도 모든 WPF 개체는 이 작업을 담당하는 발송자와 이 발송자가 있는 UI 스레드를 인식합니다. 즉, 보조 스레드에서 WPF 개체를 업데이트하려는 시도는 실패한다는 의미입니다. 이러한 작업은 DispatcherObject 클래스가 담당합니다.
DispatcherObject
WPF의 클래스 계층에서 대부분 항목은 DispatcherObject 클래스로부터(다른 클래스를 통해) 집중적으로 파생됩니다. 그림 1에서 볼 수 있는 것처럼 DispatcherObject 가상 클래스는 대부분의 WPF 클래스 계층에서 Object 바로 아래에 샌드위치 형식으로 배치되어 있습니다.
![](http://i.msdn.microsoft.com/cc163328.fig01(ko-kr).gif)
그림 1 DispatcherObject 파생
DispatcherObject 클래스에는 두 가지 주요 임무가 있으며, 첫 번째는 개체가 연결되어 있는 현재 발송자에 대한 액세스를 제공하는 것이고, 두 번째는 스레드가 개체(DispatcherObject에서 파생)를 액세스할 수 있는지 검사(CheckAccess) 및 확인(VerifyAccess)하는 메서드를 제공하는 것입니다. CheckAccess와 VerifyAccess의 차이점은 CheckAccess는 현재 스레드가 개체를 사용할 수 있는지 여부를 나타내는 부울 값을 반환하는 데 반해 VerifyAccess는 스레드가 개체를 액세스할 수 없으면 예외를 발생시킨다는 것입니다. 이러한 기본 기능이 제공되므로 모든 WPF 개체는 특정 스레드, 특히 UI 스레드에서 개체를 사용할 수 있는지 확인할 수 있게 되었습니다. 컨트롤과 같은 WPF 개체를 직접 작성하는 경우에는 모든 메서드에서 다른 작업을 수행하기 전에 VerifyAccess를 호출해야 합니다.
이렇게 하면 그림 2에서와 같이 개체가 UI 스레드에서만 사용되도록 할 수 있습니다.
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
이러한 내용을 염두에 두고 컨트롤, 창, 패널 등과 같은 DispatcherObject 파생 개체를 호출할 때는 UI 스레드에서 수행하도록 주의해야 합니다. 비 UI 스레드에서 DispatcherObject에 대한 호출을 수행하면 예외가 발생합니다. 따라서 비 UI 스레드에서 작업하는 경우에 DispatcherObject를 업데이트하려면 발송자를 사용해야 합니다.
발송자 사용
Dispatcher 클래스는 WPF의 메시지 펌프에 대한 게이트웨이를 제공하며 UI 스레드에서 처리할 작업을 라우팅하는 메커니즘도 제공합니다. 이것은 스레드 선호도 요구를 충족하는 데 필수적이지만, UI 스레드는 발송자를 통해 라우팅된 각 작업의 조각에 대해서는 차단되므로 발송자가 수행하는 작업을 작고 빨리 실행할 수 있도록 유지하는 것이 중요합니다. 큰 조각의 사용자 인터페이스의 작업을 발송자가 실행할 수 있는 작고 분리된 블록으로 나누는 것이 좋습니다. UI 스레드에서 수행할 필요가 없는 작업은 백그라운드에서 처리하도록 다른 스레드로 옮겨야 합니다.
일반적으로 Dispatcher 클래스를 사용하여 작업자 항목을 UI 스레드로 전송하여 처리하게 됩니다. 예를 들어 Thread 클래스를 사용하여 별도의 스레드에서 약간의 작업을 수행하려는 경우에는 그림 3과 같이 새 스레드에서 수행할 약간의 작업을 포함하는 ThreadStart 대리자를 만들 수 있습니다.
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
이 코드는 statusText 컨트롤(TextBlock)의 Text 속성 설정이 UI 스레드에서 호출되지 않았기 때문에 실패합니다. 코드가 TextBlock의 Text를 설정하려고 시도하면 TextBlock 클래스는 내부적으로 자체 VerifyAccess 메서드를 호출하여 호출이 UI 스레드에서 시작되었는지 확인합니다. 다른 스레드에서 호출이 시작된 것을 확인하면 예외를 발생시킵니다. 그렇다면 발송자를 사용하여 UI 스레드에서 호출을 수행하려면 어떻게 해야 할까요?
Dispatcher 클래스는 UI 스레드에서 직접 코드를 호출하는 액세스 방법을 제공합니다. 그림 4에서는 발송자의 Invoke 메서드를 통해 SetStatus라는 메서드를 호출하여 TextBlock의 Text 속성을 변경하는 방법을 보여 줍니다.
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
Invoke 호출을 위해서는 실행할 항목의 우선 순위, 수행할 작업을 설명하는 대리자, 그리고 두 번째 매개 변수의 대리자에 전달할 다른 매개 변수의 세 가지 정보가 필요합니다. Invoke를 호출하면 UI 스레드에서 호출될 대리자를 큐에 저장합니다. Invoke 메서드를 사용하면 UI 스레드에서 수행되지 전까지 작업이 차단됩니다.
동기적으로 발송자를 사용하는 방법 외에도 발송자의 BeginInvoke 메서드를 사용하여 UI 스레드에 대한 작업 항목을 비동기적으로 큐에 저장할 수 있습니다. BeginInvoke 메서드를 호출하면 작업 항목의 현재 상태 및 실행 결과(작업 항목이 실행된 다음)를 포함하여 작업 항목 실행에 대한 정보를 포함하는 DispatcherOperation 클래스의 인스턴스가 반환됩니다. 그림 5에서는 BeginInvoke 메서드와 DispatcherOperation 클래스를 사용하는 방법을 볼 수 있습니다.
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
일반적인 메시지 펌프 구현과는 달리 발송자는 작업 항목의 우선 순위 기반 큐입니다. 여기에서는 중요한 작업을 그렇지 않은 작업보다 먼저 실행할 수 있어 응답 성능이 개선됩니다. DispatchPriority 열거형에 지정된 우선 순위를 통해서 우선 순위 지정의 근본적인 특성을 파악할 수 있습니다(그림 6 참조).
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
앞서 사용했던 예와 같이 UI의 모양을 업데이트하는 작업 항목의 우선 순위는 일반적으로 항상 DispatcherPriority.Normal을 사용해야 합니다. 그러나 다른 우선 순위를 사용해야 하는 경우가 있습니다. 특히 관심을 끄는 세 가지 유휴 우선 순위(ContextIdle, ApplicationIdle 및 SystemIdle)가 있습니다. 이러한 우선 순위를 사용하여 매우 낮은 작업 부하에서만 실행될 작업 항목을 지정할 수 있습니다.
BackgroundWorker
이제 발송자가 어떻게 사용되는지에 대해 어느 정도 이해할 수 있게 되었을 것입니다. 그러나 놀랍게도 대부분의 경우에는 이를 사용할 필요가 없습니다. Microsoft는 Windows Forms 2.0에서 사용자 인터페이스 개발자의 개발 모델을 단순화하기 위해 비 UI 스레드 처리용 클래스를 도입했습니다. 그것이 BackgroundWorker 클래스입니다. 그림 7에서는 일반적인 BackgroundWorker 클래스 사용 예를 보여 줍니다.
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
BackgroundWorker 구성 요소는 WPF와도 잘 작동하는데 이것은 이 구성 요소가 내부적으로 AsyncOperationManager 클래스를 사용하며, 이는 다시 동기화를 처리하기 위해 SynchronizationContext 클래스를 사용하기 때문입니다. Windows Forms에서 AsyncOperationManager는 SynchronizationContext 클래스에서 파생되는 WindowsFormsSynchronizationContext 클래스를 제공합니다. 이와 비슷하게 ASP.NET에서는 AspNetSynchronizationContext라고 하는 SynchronizationContext의 다른 파생을 사용하여 작업합니다. SynchronizationContext에서 파생된 이러한 클래스는 메서드 호출의 스레드 간 동기화 처리할 수 있습니다.
WPF에서는 DispatcherSynchronizationContext 클래스를 통해 이 모델이 확장되었습니다. BackgroundWorker를 사용하면 스레드 간 메서드 호출을 수행하기 위해 자동으로 발송자가 사용됩니다. 좋은 소식은 이 공통 패턴에 이미 익숙할 것이기 때문에 새 WPF 프로젝트에도 계속 BackgroundWorker를 사용할 수 있다는 것입니다.
Microsoft® .NET Framework를 사용한 개발에서 주기적인 코드 실행은 일반적인 작업이지만 .NET의 타이머는 이해하기 어려운 개념 중 하나입니다. .NET Framework BCL(기본 클래스 라이브러리)에서 Timer 클래스를 찾는다면 적어도 System.Threading.Timer, System.Timers.Timer 및 System.Windows.Forms.Timer의 세 클래스를 찾을 수 있습니다. 이러한 각각의 타이머는 서로 다릅니다. Alex Calvo의 MSDN Magazine 기사에서는 이러한 각각의 Timer 클래스의 용도를 설명하고 있습니다(msdn.microsoft.com/msdnmag/issues/04/02/TimersinNET 참조).
WPF 응용 프로그램의 경우에는 발송자를 활용하는 DispatcherTimer 클래스라는 새로운 유형의 타이머가 있습니다. 다른 타이머와 마찬가지로 DispatcherTimer 클래스는 틱 간의 간격 지정은 물론 타이머의 이벤트가 발생할 때 실행할 코드를 지정할 수 있도록 지원합니다. 그림 8에서는 비교적 기초적인 DispatcherTimer 사용 예를 볼 수 있습니다.
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
DispatcherTimer 클래스는 발송자에 연결되어 있으므로 DispatcherPriority는 물론 사용할 발송자까지 지정할 수 있습니다. DispatcherTimer 클래스는 기본적으로 현재 발송자와 마찬가지로 Normal 우선 순위를 사용하지만 이러한 값은 다시 정의할 수 있습니다.
응용 프로그램의 응답 성능을 개선하기 위해 작업자 프로세스에 대한 계획을 마련하는 노력은 충분한 가치가 있습니다. 약간의 초기 연구를 수행한다면 계획을 더욱 성공적으로 수행할 수 있습니다. 시작하기 전에 "WPF 스레딩 리소스" 보충 기사에서 언급한 사이트 중 일부를 확인해 보기를 권장합니다. 응답 성능이 뛰어난 응용 프로그램을 개발하기 위해 이 기사에서 얻은 내용을 보강하는 기본 정보를 얻을 수 있을 것입니다.
'Coding > WPF, Silverlight' 카테고리의 다른 글
Secondary Monitor에 윈도 그리기 (1) | 2010.02.25 |
---|---|
WPF에서 Font ComboBox 만들기 (0) | 2009.08.12 |
WPF Threads (0) | 2009.08.09 |