다음을 통해 공유


스레딩 모델

업데이트: 2007년 11월

WPF(Windows Presentation Foundation)는 개발자들이 겪고 있는 스레딩 작업의 어려움을 해소하기 위해 개발되었습니다. 이를 이용하는 대부분의 WPF 개발자들은 둘 이상의 인터페이스를 사용하는 인터페이스를 작성할 필요가 없습니다. 다중 스레드 프로그램은 복잡하고 디버깅하기 어렵기 때문에 단일 스레드 솔루션이 있는 경우에는 다중 스레드 프로그램을 사용하지 않는 것이 좋습니다.

아무리 잘 설계되었더라도 UI 프레임워크가 모든 종류의 문제를 위한 단일 스레드 솔루션을 제공할 수는 없습니다. WPF가 이러한 능력을 어느 정도 갖추기는 했지만 여러 스레드를 사용해야 UI(사용자 인터페이스)의 응답성과 응용 프로그램 성능이 향상되는 경우가 아직도 있습니다. 이 문서에서는 몇 가지 배경 정보를 설명한 후 이러한 상황 중 일부를 살펴보고 보다 자세한 설명을 제공합니다.

이 항목에는 다음 단원이 포함되어 있습니다.

  • 개요 및 디스패처
  • 실제 사용되는 스레드: 샘플
  • 기술 세부 사항 및 장애 요소
  • 관련 항목

개요 및 디스패처

일반적으로 WPF 응용 프로그램은 두 개의 스레드로 시작하는데 하나는 렌더링을 처리하기 위한 스레드이고 다른 하나는 UI를 관리하기 위한 스레드입니다. 렌더링 스레드는 백그라운드에서 숨겨진 상태로 실행되고 UI 스레드는 입력을 받아 이벤트를 처리하고 화면을 그리며 응용 프로그램 코드를 실행합니다. 대부분의 응용 프로그램은 단일 UI 스레드를 사용하지만 스레드를 여러 개 사용하는 것이 효과적인 경우도 있습니다. 여기에 대해서는 뒷부분에서 예제를 통해 설명합니다.

UI 스레드는 Dispatcher라는 개체의 큐에 작업 항목을 대기시킵니다. Dispatcher는 우선 순위에 따라 작업 항목을 선택하고 각 작업 항목을 실행하여 완료합니다. 모든 UI 스레드에는 Dispatcher가 하나 이상 있어야 하고 각 Dispatcher는 정확히 하나의 스레드에서 작업 항목을 실행할 수 있습니다.

사용자에게 친숙하게 응답하는 응용 프로그램을 빌드하는 트릭은 작업 항목을 작게 유지하여 Dispatcher 처리량을 극대화하는 것입니다. 이 방법을 사용하면 항목은 처리를 기다리며 Dispatcher 큐에 있는 동안 오래되어 부실해지지 않습니다. 입력과 응답 사이에 시간 지연이 있을 경우 사용자가 불편해질 수 있습니다.

그렇다면 WPF 응용 프로그램에서는 큰 작업을 어떻게 처리할까요? 코드에 큰 계산이 포함되어 있거나 원격 서버에 있는 데이터베이스를 쿼리해야 한다고 가정해 보십시오. 일반적으로 이러한 경우의 해결 방법은 큰 작업을 별개의 스레드에서 처리하고 UI 스레드가 Dispatcher 큐의 항목을 처리하도록 하는 것입니다. 큰 작업이 완료되면 결과를 UI 스레드에 보고하여 표시할 수 있습니다.

지금까지 Windows에서는 UI 요소를 만든 스레드만 해당 요소에 액세스할 수 있었습니다. 이는 실행 시간이 긴 작업을 담당하는 백그라운드 스레드는 작업이 완료된 후 텍스트 상자를 업데이트할 수 없다는 것을 의미합니다. 이는 Windows에서 UI 구성 요소의 무결성을 보장하기 위한 설정입니다. 그리기 도중에 백그라운드 스레드에 의해 목록 상자의 내용이 업데이트될 경우 목록 상자가 이상하게 보일 수 있습니다.

WPF에는 이와 같은 조정을 강제 적용하는 기본 제공 상호 제외 메커니즘이 있습니다. WPF의 클래스 대부분은 DispatcherObject에서 파생됩니다. 생성 시 DispatcherObject는 현재 실행 중인 스레드에 연결된 Dispatcher에 대한 참조를 저장합니다. 그 결과 DispatcherObject는 해당 개체를 만든 스레드와 연결됩니다. 프로그램을 실행하는 동안 DispatcherObject는 해당 공용 VerifyAccess 메서드를 호출할 수 있습니다. VerifyAccess는 현재 스레드에 연결된 Dispatcher를 검사하고, 생성 시 저장된 Dispatcher 참조와 비교합니다. 두 값이 일치하지 않으면 VerifyAccess에서 예외를 throw합니다. VerifyAccessDispatcherObject에 속해 있는 모든 메서드의 시작 부분에 호출되어야 합니다.

스레드 하나만 UI를 수정할 수 있다면 백그라운드 스레드는 사용자와 어떻게 상호 작용할까요? 백그라운드 스레드는 작업을 대신 수행하도록 UI 스레드에 요청할 수 있습니다. 이를 위해 백그라운드 스레드는 UI 스레드의 Dispatcher에 작업 항목을 등록합니다. Dispatcher 클래스는 작업 항목을 등록할 수 있는 메서드로, InvokeBeginInvoke를 제공합니다. 두 메서드 모두 실행을 위해 대리자를 예약합니다. Invoke는 동기 호출로, UI 스레드가 대리자 실행을 실제로 끝낼 때까지 반환되지 않습니다. BeginInvoke는 비동기 호출이며 즉시 반환됩니다.

Dispatcher는 해당 큐의 요소를 우선 순위에 따라 순서를 지정합니다. Dispatcher 큐에 요소를 추가할 때 지정할 수 있는 10개의 수준이 있습니다. 이러한 우선 순위는 DispatcherPriority 열거에서 유지 관리됩니다. DispatcherPriority 수준에 대한 자세한 내용은 Windows SDK 문서에서 확인할 수 있습니다.

실제 사용되는 스레드: 샘플

장기 실행 계산 기능이 있는 단일 스레드 응용 프로그램

대부분의 GUI(그래픽 사용자 인터페이스)는 사용자 상호 작용에 대한 응답으로 생성되는 이벤트를 기다리면서 대부분의 시간 동안 유휴 상태로 있습니다. 신중하게 프로그래밍하면 UI의 응답성에 영향을 주지 않고 이 유휴 시간을 생산적으로 활용할 수 있습니다. WPF 스레딩 모델에서는 UI 스레드에서 수행 중인 작업이 입력에 의해 중단될 수 없습니다. 이는 보류 중인 입력 이벤트가 부실해지기 전에 정기적으로 Dispatcher로 돌아가서 해당 입력 이벤트를 처리해야 함을 의미합니다.

다음 예제를 참조하십시오.

소수 스크린 샷

이 샘플 응용 프로그램은 3부터 위쪽으로 카운트하여 소수를 검색합니다. 사용자가 시작 단추를 클릭할 경우 검색이 시작됩니다. 프로그램은 소수를 찾을 경우 사용자 인터페이스를 검색 내용으로 업데이트합니다. 사용자는 언제든지 검색을 중지할 수 있습니다.

이는 아주 간단한 작업이기는 하지만 소수 검색이 영원히 계속될 경우 약간의 문제가 될 수 있습니다. 단추의 클릭 이벤트 처리기 안에서 전체 검색을 처리한다면 UI 스레드에서는 다른 이벤트를 처리할 기회가 없을 것입니다. 이 경우 UI는 입력에 응답하거나 메시지를 처리할 수 없어 다시 그리기를 수행하거나 단추 클릭에 응답하지 않을 것입니다.

소수 검색을 별개의 스레드에서 수행할 수 있지만 이 경우 동기화 문제를 처리해야 합니다. 단일 스레드 접근 방법을 사용할 경우 발견된 가장 큰 소수를 나열하는 레이블을 직접 업데이트할 수 있습니다.

계산 작업을 관리 가능한 청크로 나눌 경우 Dispatcher에 정기적으로 돌아가서 이벤트를 처리할 수 있습니다. 다시 그리기 및 입력 처리를 수행할 수 있는 기회를 WPF에 제공할 수 있습니다.

계산 및 이벤트 처리 간의 처리 시간을 분할할 수 있는 최선의 방법은 Dispatcher에서 계산을 관리하는 것입니다. BeginInvoke 메서드를 사용하면 UI 이벤트를 가져온 동일한 큐에서 소수 검사를 예약할 수 있습니다. 이 예제에서는 소수 검사를 한 번에 하나만 예약하고 소수 검사가 완료된 즉시 다음 소수 검사를 예약합니다. 이 검사는 보류 중인 UI 이벤트가 처리된 이후에만 진행됩니다.

디스패처 큐 설명

Microsoft Word에서는 이 메커니즘을 사용하여 맞춤법 검사를 수행합니다. 맞춤법 검사는 UI 스레드의 유휴 시간을 사용하여 백그라운드에서 수행됩니다. 코드를 살펴보겠습니다.

다음 예제에서는 사용자 인터페이스를 만드는 XAML을 보여 줍니다.

<Window x:Class="SDKSamples.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="Prime Numbers" Width="260" Height="75"
    >
  <StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
    <Button Content="Start"  
            Click="StartOrStop"
            Name="startStopButton"
            Margin="5,0,5,0"
            />
    <TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
    <TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
  </StackPanel>
</Window>

다음 예제에서는 코드 숨김을 보여 줍니다.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        public delegate void NextPrimeDelegate();

        //Current number to check 
        private long num = 3;   

        private bool continueCalculating = false;

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void StartOrStop(object sender, EventArgs e)
        {
            if (continueCalculating)
            {
                continueCalculating = false;
                startStopButton.Content = "Resume";
            }
            else
            {
                continueCalculating = true;
                startStopButton.Content = "Stop";
                startStopButton.Dispatcher.BeginInvoke(
                    DispatcherPriority.Normal,
                    new NextPrimeDelegate(CheckNextNumber));
            }
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            NotAPrime = false;

            for (long i = 3; i <= Math.Sqrt(num); i++)
            {
                if (num % i == 0)
                {
                    // Set not a prime flag to ture.
                    NotAPrime = true;
                    break;
                }
            }

            // If a prime number.
            if (!NotAPrime)
            {
                bigPrime.Text = num.ToString();
            }

            num += 2;
            if (continueCalculating)
            {
                startStopButton.Dispatcher.BeginInvoke(
                    System.Windows.Threading.DispatcherPriority.SystemIdle, 
                    new NextPrimeDelegate(this.CheckNextNumber));
            }
        }

        private bool NotAPrime = false;
    }
}

다음 예제에서는 Button용 이벤트 처리기를 보여 줍니다.

private void StartOrStop(object sender, EventArgs e)
{
    if (continueCalculating)
    {
        continueCalculating = false;
        startStopButton.Content = "Resume";
    }
    else
    {
        continueCalculating = true;
        startStopButton.Content = "Stop";
        startStopButton.Dispatcher.BeginInvoke(
            DispatcherPriority.Normal,
            new NextPrimeDelegate(CheckNextNumber));
    }
}

Button에서 텍스트를 업데이트하는 것 외에도 이 처리기는 대리자를 Dispatcher 큐에 추가하여 첫 번째 소수 검사를 예약하는 작업을 수행합니다. 경우에 따라 이 이벤트 처리기에서 작업을 완료한 후 Dispatcher는 실행을 위해 이 대리자를 선택합니다.

앞에서 언급한 것처럼 BeginInvoke는 실행을 위한 대리자를 예약하는 데 사용되는 Dispatcher 멤버입니다. 이 경우에는 SystemIdle 우선 순위를 선택합니다. Dispatcher는 처리해야 할 중요한 이벤트가 없는 경우에만 이 대리자를 실행합니다. UI 응답성이 숫자 검사보다 중요합니다. 또한 이 예제에서는 숫자 검사 루틴을 나타내는 새 대리자를 전달합니다.

public void CheckNextNumber()
{
    // Reset flag.
    NotAPrime = false;

    for (long i = 3; i <= Math.Sqrt(num); i++)
    {
        if (num % i == 0)
        {
            // Set not a prime flag to ture.
            NotAPrime = true;
            break;
        }
    }

    // If a prime number.
    if (!NotAPrime)
    {
        bigPrime.Text = num.ToString();
    }

    num += 2;
    if (continueCalculating)
    {
        startStopButton.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.SystemIdle, 
            new NextPrimeDelegate(this.CheckNextNumber));
    }
}

private bool NotAPrime = false;

이 메서드는 다음 홀수가 소수인지 검사합니다. 소수일 경우 이 메서드는 검색을 반영하기 위해 bigPrime TextBlock을 직접 업데이트합니다. 구성 요소를 만드는 데 사용된 것과 동일한 스레드에서 계산이 수행되기 때문에 이 작업을 수행할 수 있습니다. 계산을 위한 별개의 스레드를 사용하도록 선택했다면 더 복잡한 동기화 메커니즘을 사용하고 UI 스레드에서 업데이트를 실행해야 합니다. 이제 이 경우에 대해 설명하겠습니다.

이 샘플의 전체 소스 코드를 보려면 장기 실행 계산 기능이 있는 단일 스레드 응용 프로그램 샘플을 참조하십시오.

백그라운드 스레드를 사용하여 차단 작업 처리

그래픽 응용 프로그램에서 차단 작업 처리는 어려울 수 있습니다. 응용 프로그램의 작동이 멈춘 것처럼 보이게 되므로 이벤트 처리기에서 차단 메서드를 호출하지는 않을 것입니다. 별도의 스레드를 사용하여 이러한 작업을 처리할 수 있지만 이 경우 작업자 스레드에서 GUI를 직접 수정할 수 없으므로 UI 스레드와 동기화해야 합니다. Invoke 또는 BeginInvoke를 사용하여 UI 스레드의 Dispatcher에 대리자를 삽입할 수 있습니다. 이러한 대리자는 UI 요소를 수정할 수 있는 권한으로 실행됩니다.

이 예제에서는 일기 예보를 수신하는 원격 프로시저 호출을 모방합니다. 별개의 작업자 스레드를 사용하여 이 호출을 실행하고 작업이 끝나면 UI 스레드의 Dispatcher에서 업데이트 메서드를 예약합니다.

날씨 UI 스크린 샷

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        // Delegates to be used in placking jobs onto the Dispatcher.
        private delegate void NoArgDelegate();
        private delegate void OneArgDelegate(String arg);

        // Storyboards for the animations.
        private Storyboard showClockFaceStoryboard;
        private Storyboard hideClockFaceStoryboard;
        private Storyboard showWeatherImageStoryboard;
        private Storyboard hideWeatherImageStoryboard;

        public Window1(): base()
        {
            InitializeComponent();
        }  

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Load the storyboard resources.
            showClockFaceStoryboard = 
                (Storyboard)this.Resources["ShowClockFaceStoryboard"];
            hideClockFaceStoryboard = 
                (Storyboard)this.Resources["HideClockFaceStoryboard"];
            showWeatherImageStoryboard = 
                (Storyboard)this.Resources["ShowWeatherImageStoryboard"];
            hideWeatherImageStoryboard = 
                (Storyboard)this.Resources["HideWeatherImageStoryboard"];   
        }

        private void ForecastButtonHandler(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            hideWeatherImageStoryboard.Begin(this);

            // Start fetching the weather forecast asynchronously.
            NoArgDelegate fetcher = new NoArgDelegate(
                this.FetchWeatherFromServer);

            fetcher.BeginInvoke(null, null);
        }

        private void FetchWeatherFromServer()
        {
            // Simulate the delay from network access.
            Thread.Sleep(4000);              

            // Tried and true method for weather forecasting - random numbers.
            Random rand = new Random();
            String weather;

            if (rand.Next(2) == 0)
            {
                weather = "rainy";
            }
            else
            {
                weather = "sunny";
            }

            // Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(
                System.Windows.Threading.DispatcherPriority.Normal,
                new OneArgDelegate(UpdateUserInterface), 
                weather);
        }

        private void UpdateUserInterface(String weather)
        {    
            //Set the weather image
            if (weather == "sunny")
            {       
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "SunnyImageSource"];
            }
            else if (weather == "rainy")
            {
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "RainingImageSource"];
            }

            //Stop clock animation
            showClockFaceStoryboard.Stop(this);
            hideClockFaceStoryboard.Begin(this);

            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;     
        }

        private void HideClockFaceStoryboard_Completed(object sender,
            EventArgs args)
        {         
            showWeatherImageStoryboard.Begin(this);
        }

        private void HideWeatherImageStoryboard_Completed(object sender,
            EventArgs args)
        {           
            showClockFaceStoryboard.Begin(this, true);
        }        
    }
}

다음은 주의해야 할 몇 가지 세부 사항입니다.

  • 단추 처리기 만들기

    private void ForecastButtonHandler(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        hideWeatherImageStoryboard.Begin(this);
    
        // Start fetching the weather forecast asynchronously.
        NoArgDelegate fetcher = new NoArgDelegate(
            this.FetchWeatherFromServer);
    
        fetcher.BeginInvoke(null, null);
    }
    

단추를 클릭할 경우 시계 그리기가 표시되고 애니메이션 효과가 시작됩니다. 단추를 비활성화합니다. 새 스레드에서 FetchWeatherFromServer 메서드를 호출한 다음 돌아옵니다. 이렇게 하면 일기 예보가 수집되기를 기다리는 동안 Dispatcher에서 이벤트를 처리할 수 있습니다.

  • 일기 예보 페치

    private void FetchWeatherFromServer()
    {
        // Simulate the delay from network access.
        Thread.Sleep(4000);              
    
        // Tried and true method for weather forecasting - random numbers.
        Random rand = new Random();
        String weather;
    
        if (rand.Next(2) == 0)
        {
            weather = "rainy";
        }
        else
        {
            weather = "sunny";
        }
    
        // Schedule the update function in the UI thread.
        tomorrowsWeather.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.Normal,
            new OneArgDelegate(UpdateUserInterface), 
            weather);
    }
    

간단히 설명하기 위해 이 예제에서는 실제로 네트워킹 코드를 사용하지 않습니다. 대신에 4초 동안 중지되는 새 스레드를 추가하여 네트워크 액세스 지연을 시뮬레이션합니다. 이 경우 원래 UI 스레드는 계속 실행되면서 이벤트에 응답합니다. 이를 보여 주기 위해 애니메이션을 실행 중인 상태로 두었으며, 최소화 및 최대화 단추도 계속 작동합니다.

  • UI 업데이트

    private void UpdateUserInterface(String weather)
    {    
        //Set the weather image
        if (weather == "sunny")
        {       
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "SunnyImageSource"];
        }
        else if (weather == "rainy")
        {
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "RainingImageSource"];
        }
    
        //Stop clock animation
        showClockFaceStoryboard.Stop(this);
        hideClockFaceStoryboard.Begin(this);
    
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;     
    }
    

UI 스레드의 Dispatcher는 시간이 있을 때 UpdateUserInterface에 대한 예약된 호출을 실행합니다. 이 메서드는 시계 애니메이션을 중지하고 일기 예보 이미지를 선택합니다. 그런 다음 이 이미지를 표시하고 “fetch forecast” 단추를 복원합니다.

이 샘플의 전체 소스 코드를 보려면 디스패처를 통한 일기 예보 서비스 시뮬레이션 샘플을 참조하십시오.

여러 창, 여러 스레드

일부 WPF 응용 프로그램에는 최상위 창이 여러 개 필요합니다. 하나의 스레드/Dispatcher 조합으로도 창을 여러 개 관리할 수 있지만 스레드를 여러 개 사용하는 것이 더 효과적인 경우가 있습니다. 특히 여러 창 중 하나에서 스레드를 독점할 가능성이 있는 경우가 여기에 해당합니다.

Windows 탐색기는 이러한 방식으로 작동합니다. 새 탐색기 창 각각은 원래 프로세스에 속하지만 독립적인 스레드의 제어하에 만들어집니다.

WPF Frame 컨트롤을 사용하면 웹 페이지를 표시할 수 있습니다. 간단한 Internet Explorer 대체물을 쉽게 만들 수 있습니다. 가장 먼저 고려해야 할 중요한 기능은 새 탐색기 창을 여는 기능입니다. 사용자가 “new window” 단추를 클릭하면 창 사본을 별도의 스레드에서 시작합니다. 이렇게 하면 여러 창 중 하나에 있는 장기 실행 또는 차단 작업이 다른 모든 창을 잠그는 일이 발생하지 않습니다.

실제로는 고유하고 복잡한 스레딩 모델이 웹 브라우저 모델에 사용됩니다. 여기서 이것을 선택한 이유는 대부분의 독자에게 익숙하기 때문입니다.

다음 예제에서는 코드를 보여 줍니다.

<Window x:Class="SDKSamples.Window1"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    Title="MultiBrowse"
    Height="600" 
    Width="800"
    Loaded="OnLoaded"
    >
  <StackPanel Name="Stack" Orientation="Vertical">
    <StackPanel Orientation="Horizontal">
      <Button Content="New Window"
              Click="NewWindowHandler" />
      <TextBox Name="newLocation"
               Width="500" />
      <Button Content="GO!"
              Click="Browse" />
    </StackPanel>

    <Frame Name="placeHolder"
            Width="800"
            Height="550"></Frame>
  </StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using System.Threading;


namespace SDKSamples
{
    public partial class Window1 : Window
    {

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
           placeHolder.Source = new Uri("https://www.msn.com");
        }

        private void Browse(object sender, RoutedEventArgs e)
        {
            placeHolder.Source = new Uri(newLocation.Text);
        }

        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();
        }
    }
}

이 코드의 다음 스레딩 세그먼트는 이 컨텍스트에서 가장 흥미로운 부분입니다.

private void NewWindowHandler(object sender, RoutedEventArgs e)
{       
    Thread newWindowThread = new Thread(new ThreadStart(ThreadStartingPoint));
    newWindowThread.SetApartmentState(ApartmentState.STA);
    newWindowThread.IsBackground = true;
    newWindowThread.Start();
}

이 메서드는 “new window” 단추를 클릭했을 때 호출됩니다. 이 메서드는 새 스레드를 만들고 비동기적으로 시작합니다.

private void ThreadStartingPoint()
{
    Window1 tempWindow = new Window1();
    tempWindow.Show();       
    System.Windows.Threading.Dispatcher.Run();
}

이 메서드는 새 스레드의 시작점입니다. 여기서는 이 스레드의 제어하에 새 창을 만듭니다. WPF에서 자동으로 새 스레드를 관리할 새 Dispatcher를 만들기 때문에 창이 작동하도록 Dispatcher를 시작하기만 하면 됩니다.

이 샘플의 전체 소스 코드를 보려면 다중 스레딩 웹 브라우저 샘플을 참조하십시오.

기술 세부 사항 및 장애 요소

스레딩을 사용하여 구성 요소 작성

Microsoft .NET Framework 개발자 가이드에는 구성 요소가 비동기 동작을 클라이언트에 노출하는 방법에 대한 패턴이 설명되어 있습니다(이벤트 기반 비동기 패턴 개요 참조). 예를 들어 FetchWeatherFromServer 메서드를 재사용 가능한 비그래픽 구성 요소에 패키징하려는 경우를 가정해 보겠습니다. 표준 Microsoft .NET Framework 패턴을 따를 경우 다음과 같이 할 수 있습니다.

public class WeatherComponent : Component
{
    //gets weather: Synchronous 
    public string GetWeather()
    {
        string weather = "";

        //predict the weather

        return weather;
    }

    //get weather: Asynchronous 
    public void GetWeatherAsync()
    {
        //get the weather
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
}

public class GetWeatherCompletedEventArgs : AsyncCompletedEventArgs
{
    public GetWeatherCompletedEventArgs(Exception error, bool canceled,
        object userState, string weather)
        :
        base(error, canceled, userState)
    {
        _weather = weather;
    }

    public string Weather
    {
        get { return _weather; }
    }
    private string _weather;
}

public delegate void GetWeatherCompletedEventHandler(object sender,
    GetWeatherCompletedEventArgs e);

GetWeatherAsync에서는 백그라운드 스레드 만들기 등과 같은 앞에 설명된 기술 중 하나를 사용하여 비동기적으로 작업을 수행하며 호출 스레드를 차단하지 않습니다.

이 패턴의 가장 중요한 부분 중 하나는 시작할 MethodNameAsync 메서드를 호출한 동일한 스레드에서 MethodNameCompleted 메서드를 호출하는 것입니다. WPF를 사용하면 CurrentDispatcher를 저장하여 이를 쉽게 수행할 수 있지만 비그래픽 구성 요소를 WPF 응용 프로그램에서만 사용할 수 있고 Windows Forms 또는 ASP.NET 프로그램에서는 사용할 수 없습니다.

DispatcherSynchronizationContext 클래스는 이 요구 사항을 해결합니다. 이 클래스를 다른 UI 프레임워크에서도 작동하는 단순화된 버전의 Dispatcher라고 생각할 수 있습니다.

public class WeatherComponent2 : Component
{
    public string GetWeather()
    {
        return fetchWeatherFromServer();
    }

    private DispatcherSynchronizationContext requestingContext = null;

    public void GetWeatherAsync()
    {
        if (requestingContext != null)
            throw new InvalidOperationException("This component can only handle 1 async request at a time");

        requestingContext = (DispatcherSynchronizationContext)DispatcherSynchronizationContext.Current;

        NoArgDelegate fetcher = new NoArgDelegate(this.fetchWeatherFromServer);

        // Launch thread
        fetcher.BeginInvoke(null, null);
    }

    private void RaiseEvent(GetWeatherCompletedEventArgs e)
    {
        if (GetWeatherCompleted != null)
            GetWeatherCompleted(this, e);
    }

    private string fetchWeatherFromServer()
    {
        // do stuff
        string weather = "";

        GetWeatherCompletedEventArgs e =
            new GetWeatherCompletedEventArgs(null, false, null, weather);

        SendOrPostCallback callback = new SendOrPostCallback(DoEvent);
        requestingContext.Post(callback, e);
        requestingContext = null;

        return e.Weather;
    }

    private void DoEvent(object e)
    {
        //do stuff
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
    public delegate string NoArgDelegate();
}

중첩된 펌프

UI 스레드를 완전하게 잠그는 것이 불가능한 경우도 종종 있습니다. MessageBox 클래스의 Show 메서드를 살펴보도록 하겠습니다. Show는 사용자가 확인 단추를 클릭해야만 반환됩니다. 그러나 이 메서드는 메시지 루프가 있어야 대화형 창이 될 수 있는 창을 만듭니다. 사용자가 확인을 클릭할 때까지 기다리는 동안 원래 응용 프로그램 창은 사용자 입력에 응답하지 않지만 그리기 메시지는 계속해서 처리합니다. 원래 창은 가려졌다가 다시 표시될 때 다시 그려집니다.

"확인" 단추가 있는 MessageBox

메시지 상자 창을 담당하는 스레드가 있어야 합니다. WPF에서 메시지 상자 창을 위한 전용 스레드를 새로 만들 수 있지만 이 스레드는 원래 창에서 비활성화된 요소를 그릴 수 없습니다(상호 제외에 대한 설명 참조). 이렇게 하는 대신 WPF는 중첩 메시지 처리 시스템을 사용합니다. Dispatcher 클래스에는 응용 프로그램의 현재 실행 지점을 저장한 다음 새 메시지 루프를 시작하는 PushFrame이라는 특수한 메서드가 포함되어 있습니다. 중첩 메시지 루프가 끝나면 원래 PushFrame 호출 이후 시점부터 실행이 다시 시작됩니다.

이 경우 PushFrameMessageBox.Show에 대한 호출에서 프로그램 컨텍스트를 유지 관리하고 새 메시지 루프를 시작하여 배경 창을 다시 그리며 메시지 상자 창에 대한 입력을 처리합니다. 사용자가 확인을 클릭하고 팝업 창을 지울 경우 중첩 루프가 종료하고 Show에 대한 호출 이후에 컨트롤이 다시 시작됩니다.

라우트된 부실 이벤트

WPF의 라우트된 이벤트 시스템은 이벤트가 발생할 경우 이벤트 트리에 알립니다.

<Canvas MouseLeftButtonDown="handler1" 
        Width="100"
        Height="100"
        >
  <Ellipse Width="50"
           Height="50"
           Fill="Blue" 
           Canvas.Left="30"
           Canvas.Top="50" 
           MouseLeftButtonDown="handler2"
           />
</Canvas>

타원 위에서 왼쪽 마우스 단추를 누를 경우 handler2가 실행됩니다. handler2가 완료되면 이벤트가 Canvas 개체에 전달되고 이 개체는 handler1을 사용하여 이벤트를 처리합니다. 이는 handler2에서 이벤트 개체를 처리된 것으로 명시적으로 표시하지 않을 경우에만 발생합니다.

handler2에서 이 이벤트를 처리하는 데 매우 오래 걸릴 수 있습니다. handler2는 PushFrame을 사용하여 몇 시간 동안 반환되지 않는 중첩 메시지 루프를 시작할 수도 있습니다. 이 메시지 루프가 완료되었을 때 handler2가 이벤트를 처리된 것으로 표시하지 않을 경우 이벤트는 매우 오래된 경우에도 트리의 위로 전달됩니다.

재진입 및 잠금

CLR(공용 언어 런타임)의 잠금 메커니즘은 생각하는 것처럼 동작하지 않습니다. 잠금을 요청하면 스레드에서 작업을 완전하게 중단할 것으로 생각하겠지만 실제로 스레드에서는 우선 순위가 높은 메시지를 계속해서 수신하고 처리합니다. 이렇게 하면 교착 상태를 방지하고 인터페이스가 최소한으로 응답하도록 할 수 있지만 미세한 버그가 발생할 가능성이 있습니다. 대부분의 경우에는 이러한 버그에 신경쓰지 않아도 되지만 아주 드물게는 버그에 대해 알아 둘 필요가 있으며 일반적으로 Win32 창 메시지 또는 COM STA 구성 요소와 관련된 경우가 여기에 해당합니다.

개발자는 여러 스레드에서 UI에 액세스하는 경우가 절대 없다는 가정하에 작업하기 때문에 대부분의 인터페이스는 스레드 안정성을 고려하지 않고 빌드됩니다. 이 경우 단일 스레드가 예기치 않은 시점에 환경을 변화시켜 DispatcherObject 상호 제외 메커니즘이 해결해야 하는 부정적인 문제가 발생할 수 있습니다. 다음 의사 코드를 살펴보겠습니다.

스레딩 재입력 가능성 다이어그램

대부분의 경우에는 이것이 문제가 되지 않지만 WPF에서 이러한 예기치 않은 재진입이 실제로 문제를 일으키는 경우가 있습니다. 따라서 특정 주요 시간에 WPF는 일반적인 CLR 잠금 대신 재진입 없는 WPF 잠금을 사용하도록 스레드의 잠금 명령을 변경하는 DisableProcessing을 호출합니다.

그렇다면 CLR 팀에서 이 동작을 선택한 이유는 무엇일까요? 이 결정은 COM STA 개체 및 종료 스레드와 관련이 있습니다. 개체에서 가비지가 수집되면 해당 Finalize 메서드는 UI 스레드가 아니라 전용 종료자 스레드에서 실행됩니다. 문제는 UI 스레드에서 만들어진 COM STA 개체는 UI 개체에서만 삭제될 수 있다는 점입니다. CLR은 BeginInvoke와 동일한 기능을 수행합니다. 이 경우 Win32의 SendMessage를 사용합니다. 그러나 UI 스레드가 사용 중이면 종료자 스레드가 중단되어 COM STA 개체를 삭제할 수 없으므로 심각한 메모리 누수가 발생합니다. 이러한 이유 때문에 CLR 팀에서 잠금 방식이 현재와 같이 작동하도록 결정을 내린 것입니다.

WPF에 대한 작업은 메모리 누수를 다시 초래하지 않고 예기치 않은 재진입을 방지하는 것이며 여기서 재진입을 모든 곳에서 차단하지 않은 것도 이러한 이유 때문입니다.

참고 항목

작업

장기 실행 계산 기능이 있는 단일 스레드 응용 프로그램 샘플

디스패처를 통한 일기 예보 서비스 시뮬레이션 샘플

다중 스레딩 웹 브라우저 샘플