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


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

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

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

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

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

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

Разработка приложений 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 может вызвать свой открытый метод VerifyAccessVerifyAccess проверяет объект Dispatcher, связанный с текущим потоком, и сравнивает его со ссылкой на Dispatcher, которая сохраняется во время создания. Если они не совпадают, метод VerifyAccess вызывает исключение. VerifyAccess предназначен для вызова в начале каждого метода, принадлежащего объекту DispatcherObject.

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

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

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

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

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

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

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

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

При всей простоте операции поиск простых чисел может происходить бесконечно, что представляет некоторые трудности. Если бы обработка всех операций поиска выполнялась внутри обработчика событий нажатия кнопки, поток 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>
<Window x:Class="SDKSamples.MainWindow"
    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>

Следующий пример демонстрирует фоновый код.

Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Threading
Imports System.Threading

Namespace SDKSamples
    Partial Public Class MainWindow
        Inherits Window
        Public Delegate Sub NextPrimeDelegate()

        'Current number to check 
        Private num As Long = 3

        Private continueCalculating As Boolean = False

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
            If continueCalculating Then
                continueCalculating = False
                startStopButton.Content = "Resume"
            Else
                continueCalculating = True
                startStopButton.Content = "Stop"
                startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
            End If
        End Sub

        Public Sub CheckNextNumber()
            ' Reset flag.
            NotAPrime = False

            For i As Long = 3 To Math.Sqrt(num)
                If num Mod i = 0 Then
                    ' Set not a prime flag to true.
                    NotAPrime = True
                    Exit For
                End If
            Next

            ' If a prime number.
            If Not NotAPrime Then
                bigPrime.Text = num.ToString()
            End If

            num += 2
            If continueCalculating Then
                startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
            End If
        End Sub

        Private NotAPrime As Boolean = False
    End Class
End Namespace
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 true.
                    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 Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
    If continueCalculating Then
        continueCalculating = False
        startStopButton.Content = "Resume"
    Else
        continueCalculating = True
        startStopButton.Content = "Stop"
        startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
    End If
End Sub
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 Sub CheckNextNumber()
    ' Reset flag.
    NotAPrime = False

    For i As Long = 3 To Math.Sqrt(num)
        If num Mod i = 0 Then
            ' Set not a prime flag to true.
            NotAPrime = True
            Exit For
        End If
    Next

    ' If a prime number.
    If Not NotAPrime Then
        bigPrime.Text = num.ToString()
    End If

    num += 2
    If continueCalculating Then
        startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf Me.CheckNextNumber))
    End If
End Sub

Private NotAPrime As Boolean = False
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 true.
            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. Эта ситуация будет продемонстрирована далее.

Полный исходный код примера см. в разделе Single-Threaded Application with Long-Running Calculation Sample ("Пример однопоточного приложения с длительным выполнением вычислений").

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

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

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

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


Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Media
Imports System.Windows.Media.Animation
Imports System.Windows.Media.Imaging
Imports System.Windows.Shapes
Imports System.Windows.Threading
Imports System.Threading

Namespace SDKSamples
    Partial Public Class Window1
        Inherits Window
        ' Delegates to be used in placking jobs onto the Dispatcher.
        Private Delegate Sub NoArgDelegate()
        Private Delegate Sub OneArgDelegate(ByVal arg As String)

        ' Storyboards for the animations.
        Private showClockFaceStoryboard As Storyboard
        Private hideClockFaceStoryboard As Storyboard
        Private showWeatherImageStoryboard As Storyboard
        Private hideWeatherImageStoryboard As Storyboard

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Load the storyboard resources.
            showClockFaceStoryboard = CType(Me.Resources("ShowClockFaceStoryboard"), Storyboard)
            hideClockFaceStoryboard = CType(Me.Resources("HideClockFaceStoryboard"), Storyboard)
            showWeatherImageStoryboard = CType(Me.Resources("ShowWeatherImageStoryboard"), Storyboard)
            hideWeatherImageStoryboard = CType(Me.Resources("HideWeatherImageStoryboard"), Storyboard)
        End Sub

        Private Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            ' Change the status image and start the rotation animation.
            fetchButton.IsEnabled = False
            fetchButton.Content = "Contacting Server"
            weatherText.Text = ""
            hideWeatherImageStoryboard.Begin(Me)

            ' Start fetching the weather forecast asynchronously.
            Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)

            fetcher.BeginInvoke(Nothing, Nothing)
        End Sub

        Private Sub FetchWeatherFromServer()
            ' Simulate the delay from network access.
            Thread.Sleep(4000)

            ' Tried and true method for weather forecasting - random numbers.
            Dim rand As New Random()
            Dim weather As String

            If rand.Next(2) = 0 Then
                weather = "rainy"
            Else
                weather = "sunny"
            End If

            ' Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
        End Sub

        Private Sub UpdateUserInterface(ByVal weather As String)
            'Set the weather image
            If weather = "sunny" Then
                weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
            ElseIf weather = "rainy" Then
                weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
            End If

            'Stop clock animation
            showClockFaceStoryboard.Stop(Me)
            hideClockFaceStoryboard.Begin(Me)

            'Update UI text
            fetchButton.IsEnabled = True
            fetchButton.Content = "Fetch Forecast"
            weatherText.Text = weather
        End Sub

        Private Sub HideClockFaceStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showWeatherImageStoryboard.Begin(Me)
        End Sub

        Private Sub HideWeatherImageStoryboard_Completed(ByVal sender As Object, ByVal args As EventArgs)
            showClockFaceStoryboard.Begin(Me, True)
        End Sub
    End Class
End Namespace
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 Sub ForecastButtonHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
                ' Change the status image and start the rotation animation.
                fetchButton.IsEnabled = False
                fetchButton.Content = "Contacting Server"
                weatherText.Text = ""
                hideWeatherImageStoryboard.Begin(Me)
    
                ' Start fetching the weather forecast asynchronously.
                Dim fetcher As New NoArgDelegate(AddressOf Me.FetchWeatherFromServer)
    
                fetcher.BeginInvoke(Nothing, Nothing)
            End Sub
    
    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 Sub FetchWeatherFromServer()
                ' Simulate the delay from network access.
                Thread.Sleep(4000)
    
                ' Tried and true method for weather forecasting - random numbers.
                Dim rand As New Random()
                Dim weather As String
    
                If rand.Next(2) = 0 Then
                    weather = "rainy"
                Else
                    weather = "sunny"
                End If
    
                ' Schedule the update function in the UI thread.
                tomorrowsWeather.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, New OneArgDelegate(AddressOf UpdateUserInterface), weather)
            End Sub
    
    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);
    }
    

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

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

  • Обновление UI

            Private Sub UpdateUserInterface(ByVal weather As String)
                'Set the weather image
                If weather = "sunny" Then
                    weatherIndicatorImage.Source = CType(Me.Resources("SunnyImageSource"), ImageSource)
                ElseIf weather = "rainy" Then
                    weatherIndicatorImage.Source = CType(Me.Resources("RainingImageSource"), ImageSource)
                End If
    
                'Stop clock animation
                showClockFaceStoryboard.Stop(Me)
                hideClockFaceStoryboard.Begin(Me)
    
                'Update UI text
                fetchButton.IsEnabled = True
                fetchButton.Content = "Fetch Forecast"
                weatherText.Text = weather
            End Sub
    
    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 потока UI есть время, он выполняет запланированный вызов метода UpdateUserInterface. Этот метод останавливает анимацию часов и выбирает изображение для описания погоды. Он отображает это изображение и восстанавливает кнопку "Получить прогноз погоды".

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

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

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

Элемент управления Frame системы WPF позволяет отображать веб-страницы. Таким образом можно легко создать простую замену браузеру 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>

Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Data
Imports System.Windows.Threading
Imports System.Threading


Namespace SDKSamples
    Partial Public Class Window1
        Inherits Window

        Public Sub New()
            MyBase.New()
            InitializeComponent()
        End Sub

        Private Sub OnLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
           placeHolder.Source = New Uri("https://www.msn.com")
        End Sub

        Private Sub Browse(ByVal sender As Object, ByVal e As RoutedEventArgs)
            placeHolder.Source = New Uri(newLocation.Text)
        End Sub

        Private Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
            newWindowThread.SetApartmentState(ApartmentState.STA)
            newWindowThread.IsBackground = True
            newWindowThread.Start()
        End Sub

        Private Sub ThreadStartingPoint()
            Dim tempWindow As New Window1()
            tempWindow.Show()
            System.Windows.Threading.Dispatcher.Run()
        End Sub
    End Class
End Namespace
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 Sub NewWindowHandler(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Dim newWindowThread As New Thread(New ThreadStart(AddressOf ThreadStartingPoint))
            newWindowThread.SetApartmentState(ApartmentState.STA)
            newWindowThread.IsBackground = True
            newWindowThread.Start()
        End Sub
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 Sub ThreadStartingPoint()
            Dim tempWindow As New Window1()
            tempWindow.Show()
            System.Windows.Threading.Dispatcher.Run()
        End Sub
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
        Inherits Component
        'gets weather: Synchronous 
        Public Function GetWeather() As String
            Dim weather As String = ""

            'predict the weather

            Return weather
        End Function

        'get weather: Asynchronous 
        Public Sub GetWeatherAsync()
            'get the weather
        End Sub

        Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
    End Class

    Public Class GetWeatherCompletedEventArgs
        Inherits AsyncCompletedEventArgs
        Public Sub New(ByVal [error] As Exception, ByVal canceled As Boolean, ByVal userState As Object, ByVal weather As String)
            MyBase.New([error], canceled, userState)
            _weather = weather
        End Sub

        Public ReadOnly Property Weather() As String
            Get
                Return _weather
            End Get
        End Property
        Private _weather As String
    End Class

    Public Delegate Sub GetWeatherCompletedEventHandler(ByVal sender As Object, ByVal e As GetWeatherCompletedEventArgs)
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, который работает также другими средами UI.

    Public Class WeatherComponent2
        Inherits Component
        Public Function GetWeather() As String
            Return fetchWeatherFromServer()
        End Function

        Private requestingContext As DispatcherSynchronizationContext = Nothing

        Public Sub GetWeatherAsync()
            If requestingContext IsNot Nothing Then
                Throw New InvalidOperationException("This component can only handle 1 async request at a time")
            End If

            requestingContext = CType(DispatcherSynchronizationContext.Current, DispatcherSynchronizationContext)

            Dim fetcher As New NoArgDelegate(AddressOf Me.fetchWeatherFromServer)

            ' Launch thread
            fetcher.BeginInvoke(Nothing, Nothing)
        End Sub

        Private Sub [RaiseEvent](ByVal e As GetWeatherCompletedEventArgs)
            RaiseEvent GetWeatherCompleted(Me, e)
        End Sub

        Private Function fetchWeatherFromServer() As String
            ' do stuff
            Dim weather As String = ""

            Dim e As New GetWeatherCompletedEventArgs(Nothing, False, Nothing, weather)

            Dim callback As New SendOrPostCallback(AddressOf DoEvent)
            requestingContext.Post(callback, e)
            requestingContext = Nothing

            Return e.Weather
        End Function

        Private Sub DoEvent(ByVal e As Object)
            'do stuff
        End Sub

        Public Event GetWeatherCompleted As GetWeatherCompletedEventHandler
        Public Delegate Function NoArgDelegate() As String
    End Class
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. Рассмотрим метод Show класса MessageBoxShow возвращает значение лишь после того, как пользователь нажмет кнопку "ОК". Однако он создает окно, которое должно иметь цикл обработки сообщений, чтобы быть интерактивным. Ожидая, когда пользователь нажмет кнопку "ОК", исходное окно приложения не отвечает на ввод данных пользователем. Тем не менее оно продолжает обрабатывать сообщения отображения. Исходное окно перерисовывается при его перекрытии и выведении. 

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

Повторный вход и блокировка

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

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

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

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

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

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

См. также

Другие ресурсы

Single-Threaded Application with Long-Running Calculation Sample