Поделиться через


Модель потоков

Обновлен: Ноябрь 2007

Приложение Windows Presentation Foundation (WPF) создано для того, чтобы помочь разработчикам избежать трудностей при разработке потоков. В результате, большинству разработчиков WPF не требуется писать интерфейс, использующий более одного потока. Поскольку многопотоковые программы являются сложными и трудно отлаживаемыми, их следует избегать, если существуют однопотоковые решения.

Независимо от качества архитектуры, никакая структура Пользовательский интерфейс не сможет предложить однопотоковое решение для каждого типа задач. Приложение WPF достаточно близко приблизилось к решению этой проблемы, но по-прежнему есть ситуации, в которых несколько потоков улучшают отклик пользовательский интерфейс или производительность приложения. После обсуждения некоторых исходных материалов, в данном документе рассматриваются подобные ситуации и затем завершается обсуждением некоторых сведений нижнего уровня.

В этом разделе содержатся следующие подразделы.

  • Общие сведения и Dispatcher
  • Потоки в действии: примеры
  • Технические подробности и важные моменты
  • Связанные разделы

Общие сведения и Dispatcher

Как правило, приложения WPF начинают работу с двумя потоками: один — для обработки отрисовки, и другой — для управления Пользовательский интерфейс. Поток отрисовки эффективно выполняется скрыто в фоновом режиме, пока поток Пользовательский интерфейс получает входные данные, обрабатывает события, выводит изображение на экран и выполняет код приложения. Большинство приложений используют один поток Пользовательский интерфейс, хотя в некоторых случаях лучше использовать несколько. Позже это будет обсуждено на примере.

Поток Пользовательский интерфейс ставит в очередь рабочие элементы внутри объекта, называемого Dispatcher. Объект Dispatcher выбирает рабочие элементы на основе приоритетов и выполняет каждый из них до завершения.  Каждый поток Пользовательский интерфейс должен иметь, по крайней мере, один объект Dispatcher, и каждый объект Dispatcher может выполнять рабочие элементы только в одном потоке.

Условием для построения быстро реагирующих, понятные пользователю приложений является максимальное повышение производительности Dispatcher, сохранения рабочие элементы небольшими. При таком методе элементы никогда не устаревают в очереди Dispatcher в ожидании обработки. Любая задержка между входными данными и ответами может разочаровывать пользователя.

Как тогда приложения WPF должны обрабатывать большие операции? Что если код включает большие вычисления или требуется запрос к базе данных на удаленном сервере? Обычно большие операции обрабатываются в отдельном потоке, оставляя поток Пользовательский интерфейс свободным для обслуживания элементов в очереди Dispatcher. После завершения большой операции, она может передать результат обратно в поток Пользовательский интерфейс для отображения.

Исторически, Windows позволяет элементам Пользовательский интерфейс быть доступными только в потоке, который создал их. Это означает, что фоновый поток, отвечающий за некоторую длительную задачу, при его завершении не может обновить текстовые поле. Windows выполняет это для обеспечения целостности компонентов пользовательского интерфейса. Поле со списком может выглядеть странно, если его содержимое было обновлено фоновым потоком в процессе отображения.

WPF имеет встроенный взаимный механизм исключений, который осуществляет эту координацию. Большинство классов в приложении WPF являются производными от класса DispatcherObject. При построении DispatcherObject хранит ссылку на объект Dispatcher, связанный с текущим выполняемым потоком. В результате, DispatcherObject связывается с потоком, который его создал. Во время выполнения программы DispatcherObject может вызвать свой открытый метод VerifyAccess. VerifyAccess проверяет Dispatcher, связанный с текущим потоком, и сравнивает его со ссылкой Dispatcher, которая хранится во время построения. Если они не совпадают, VerifyAccess вызывает исключение. VerifyAccess предназначен для вызова в начале каждого метода, принадлежащего объекту DispatcherObject.

Если только один поток может изменить Пользовательский интерфейс, как фоновые потоки взаимодействуют с пользователем? Фоновый поток может запросить поток Пользовательский интерфейс для выполнения операции от его имени. Он делает это путем регистрации рабочего элемента с объектом Dispatcher потока Пользовательский интерфейс. Класс Dispatcher предоставляет два способа регистрации рабочих элементов: Invoke и BeginInvoke. Оба метода назначают делегат для выполнения. Метод Invoke является синхронным вызовом — он не возвращается до тех пор, пока поток Пользовательский интерфейс не закончит выполнение делегата. Метод BeginInvoke является асинхронным и немедленно возвращается.

Объект Dispatcher упорядочивает элементы в своей очереди по приоритету. Существуют десять уровней, которые могут быть указаны при добавлении элемента в очередь Dispatcher. Эти приоритеты сохраняются в перечислении DispatcherPriority. Подробные сведения об уровнях DispatcherPriority можно найти в Windows SDK документации.

Потоки в действии: примеры

Пример однопоточного приложения с длительным выполнением вычислений

Большинство GUI (graphical user interfaces — графические интерфейсы пользователя) тратят большую часть своего времени, простаивая в ожидании события, которое создается в ответ на действие пользователя. При внимательном программировании это время простоя можно использовать конструктивно, не затрагивая активного состояния. Пользовательский интерфейс. Потоковая модель WPF не позволяет вводу прерывать операцию, которая происходит в потоке Пользовательский интерфейс. Это означает, что периодически требуется возвращаться к объекту Dispatcher, чтобы обработать отложенные события ввода, прежде чем они станут устаревшими.

Рассмотрим следующий пример:

Снимок экрана начальных чисел

Это простое приложение ищет простые числа, начиная от трех и далее. При нажатии кнопки Пуск поиск начинается. Когда программа находит простое число, она обновляет пользовательский интерфейс. В любой точке пользователь может остановить поиск.

При всей простоте операции поиск простых чисел может происходить бесконечно, что представляет некоторые трудности. Если бы обработка всего поиска выполнялась внутри обработчика событий нажатия кнопки, поток Пользовательский интерфейс никогда бы не получил возможность для обработки других событий. Пользовательский интерфейс не мог бы ответить на входные данные или сообщения процесса. Он бы никогда не обновил отображение и не ответил бы на нажатие кнопки.

Можно провести поиск простого числа в отдельном потоке, но тогда пришлось бы иметь дело с проблемами синхронизации. С помощью однопотокового подхода можно непосредственно обновить подпись, в которой перечислено наибольшее простое число.

Если разбить задачу вычисления на управляемые фрагменты, можно периодически возвращаться к объекту Dispatcher и событиям обработки. Можно дать приложению WPF возможность обновлять и обрабатывать ввод.

Лучшим способом разбиения времени обработки между вычислением и обработкой события является управление вычислением из объекта Dispatcher. С помощью метода BeginInvoke можно запланировать проверку простого числа в той же очереди, из которой приходят события Пользовательский интерфейс. В приведенном примере запланирована проверка только одного простого числа в каждый момент времени. После завершения проверки простого числа немедленно планируется следующая проверка. Эта проверка продолжается только после ожидающих обработки событий Пользовательский интерфейс.

Иллюстрация очереди отправителя

С помощью этого механизма приложение Microsoft Word выполняет проверку орфографии. Проверка орфографии выполняется в фоновом режиме, используя время простоя потока Пользовательский интерфейс. Давайте посмотрим на код.

В следующем примере показан код 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 будет выполнять этот делегат только при отсутствии важных событий для обработки. Быстрота реагирования Пользовательский интерфейс более важна, чем проверка числа. Также передается новый делегат, представляющий подпрограмму проверки числа.

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, чтобы отразить его результат поиска. Мы можем сделать так потому, что вычисление происходит в том же потоке, который был использован для создания компонента. Если использовать отдельный поток для вычислений, пришлось бы применить более сложный механизм синхронизации и выполнять обновления в потоке Пользовательский интерфейс. Эта ситуация будет продемонстрирована далее.

Полный исходный код для этого примера содержится в разделе Пример однопотокового приложения с длительным выполнением вычислений.

Обработка блокирующей операции с фоновым потоком

Обработка блокировки операций в графическом приложении может оказаться трудной задачей. Мы не будем вызывать методы блокировки из обработчиков событий, так как приложение будет остановлено. Можно использовать отдельный поток для обработки этих операций, но затем нужно будет синхронизироваться с потоком Пользовательский интерфейс, поскольку нельзя непосредственно изменить Графический интерфейс пользователя из рабочего потока. Можно использовать Invoke или BeginInvoke для вставки делегатов в объект Dispatcher потока Графический интерфейс. Наконец, эти делегаты будут выполнены с разрешением на изменение элементов Графический интерфейс.

В этом примере мы имитируем вызов удаленной процедуры, который получает прогноз погоды. Мы используем отдельный рабочий поток для выполнения этого вызова и планируем метод обновления в объекте Dispatcher потока Пользовательский интерфейс при завершении.

Снимок экрана пользовательского интерфейса Weather

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

Для просты мы фактически не используем никакого сетевого кода в данном примере. Вместо этого, мы моделируем задержку доступа к сети, задав для нашего нового потока спящий режим в течение четырех секунд. В течение этого времени исходный поток Графический интерфейс по-прежнему выполняется и отвечает на события. Чтобы показать это, была оставлена запущенная анимация, и кнопки свертывания и развертывания также продолжают работать.

  • Обновление Пользовательский интерфейс

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

Когда объект Dispatcher в потоке Графический интерфейс содержит время, он выполняет запланированный вызов в UpdateUserInterface. Данный метод останавливает часы анимации и выбирает изображение для описания погоды. Он отображает это изображение и восстанавливает кнопку "выборка прогноза погоды".

Полный исходный код для этого примера содержится в разделе Пример имитации службы погоды с помощью диспетчера.

Несколько окон, несколько потоков

Некоторые приложения WPF требуют нескольких окон верхнего уровня. Это полностью допустимо для одного сочетания поток/Dispatcher для управления несколькими окнами, но иногда несколько потоков выполняют задание лучше. Это особенно верно, когда существует возможность, что одно из окон будет захватывать поток.

Windows Explorer работает таким образом. Каждое новое окно обозревателя принадлежит исходному процессу, однако оно создано под управлением независимого потока.

С помощью элемента управления WPF Frame можно отобразить веб-страницы. Можно легко создать простое замещение Графический интерфейс. Начинаем с важной функции: возможности открыть новое окно обозревателя. Когда пользователь нажимает кнопку "новое окно", запускается копия окна в отдельном потоке. Таким образом, долго выполняющиеся или блокирующие операции в одном из окон не блокируют все остальные окна.

На самом деле, веб-обозревателя имеет свою собственную сложную поточную модель. Мы выбрали его, поскольку он знаком большинству читателей.

В следующем примере показан код.

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

Этот метод вызывается, когда нажата кнопка "новое окно". Она создает новый поток и запускает его в асинхронном режиме.

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 использовал бы один из методов, описанных выше, таких как создание фонового потока, для работы в асинхронном режиме, не блокируя вызов потока.

Одной из наиболее важных частей этого шаблона является вызов метода MethodNameCompleted в том же потоке, который вызвал метод MethodNameAsync. Это можно сделать с помощью WPF довольно просто, сохранения CurrentDispatcher, но затем неграфический компонент может использоваться только в приложениях WPF, а не в программах Windows Forms или ASP.NET. 

Класс DispatcherSynchronizationContext создан для решения этой задачи — представляйте его упрощенную версию 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();
}

Вложенная накачка

Иногда не выполнима полная блокировка потока Графический интерфейс. Рассмотрим метод Show класса MessageBox. Метод Show не возвращается до тех пор, пока пользователь не нажмет кнопку "ОК". Он, однако, создает окно, которое должно иметь цикл обработки сообщений, чтобы быть интерактивным. Во время ожидания, когда пользователь нажмет кнопку "ОК", исходное окно приложения не отвечает на ввод данных пользователем. Оно, тем не менее, продолжает обрабатывать сообщения отображения. Исходное окно перерисовывается при его перекрытии и выведении. 

MessageBox с кнопкой “ОК”

Некоторые потоки должны отвечать за окно сообщения. ПриложениеWPF могло бы создать новый поток только для окна сообщения, но этот поток не может отображать отключенные элементы в исходном окне (вспомните предыдущее обсуждение взаимного исключения). Вместо этого, WPF использует систему обработки вложенных сообщений. Класс Dispatcher содержит специальный метод, называемый PushFrame, который хранит текущую точку выполнения приложения и затем начинает новый цикл обработки сообщений. После завершения цикла обработки вложенных сообщений выполнение возобновляется после исходного вызова PushFrame.

В этом случае, PushFrame поддерживает программный контекст при вызове MessageBox. 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 (common language runtime) не работает в точности, как это можно представить; можно было бы ожидать, что поток завершит операцию полностью, запрашивая блокировку. В действительности, поток продолжает получать и обрабатывать сообщения с высоким приоритетом. Это помогает избежать взаимоблокировок и делает интерфейсы максимально быстрореагирующими, но приводит к вероятным незначительным ошибкам.  В большинстве случаях нет необходимости что-либо знать об этом, но иногда (как правило, в случаях, включающих сообщения окон Win32 или компоненты COM STA) знания могут потребоваться.

Большинство интерфейсов построено без учета безопасности потока, так как разработчик работает, предполагая, что доступ к Пользовательский интерфейс всегда выполняется не более чем одним потоком. В этом случае, предполагается, что побочные эффекты, вносимые одним потоком при изменении среды в непредвиденное время, устраняет механизм взаимного исключения DispatcherObject. Рассмотрим следующий псевдокод:

Схема повторного входа потоков

Большую часть времени все работает правильно, но в какой-то момент в приложении WPF такой непредвиденный повторный вход может действительно вызвать проблемы. Таким образом, в некий ключевой момент, WPF вызывает метод DisableProcessing, который меняет инструкцию блокировки для этого потока, чтобы использовать свободную от повторного входа блокировку WPF, вместо обычной блокировки CLR. 

Так почему же группа CLR выбрала такое поведение? Это было связано с объектами COM STA и завершением потока. Если объект является сборщиком мусора, его метод Finalize выполняется в выделенном потоке завершения, а не в потоке Графический интерфейс. Именно здесь лежит проблема, поскольку объект COM STA, созданный в потоке Графический интерфейс, может быть удален только в потоке Графический интерфейс. Спецификация CLR не эквивалентна методу BeginInvoke (в данном случае, использующему SendMessage Win32). Но если поток Графический интерфейс занят, поток завершения останавливается, и объект COM STA не может быть удален, что создает серьезную "утечку памяти". Поэтому группа CLR создала сложный вызов для создания блокировки работы таким образом.  

Задача приложения WPF — избежать непредвиденного повторного входа без внесения "утечки памяти", вот почему мы не блокируем где-либо повторный вход.

См. также

Задачи

Пример однопотокового приложения с длительным выполнением вычислений

Пример имитации службы погоды с помощью диспетчера

Пример многопоточного веб-обозревателя