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

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

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

Примечание.

В данном разделе обсуждается работа с потоками и использование метода InvokeAsync для асинхронных вызовов. Метод InvokeAsync принимает или Func<TResult> в Action качестве параметра и возвращает или возвращает DispatcherOperation или DispatcherOperation<TResult>имеет Task свойство. Ключевое слово await можно использовать как для DispatcherOperation, так и для связанной задачи Task. Если необходимо синхронное ожидание объекта Task, возвращаемого DispatcherOperation или DispatcherOperation<TResult>, вызывайте метод расширения DispatcherOperationWait. Вызов Task.Wait приведет к взаимоблокировке. Дополнительные сведения об использовании Task для выполнения асинхронных операций см. в статье Асинхронное программирование на основе задач.

Чтобы сделать синхронный вызов, используйте Invoke метод, который также имеет перегрузки, принимающее делегат или ActionFunc<TResult> параметр.

Обзор и диспетчер

Как правило, приложения 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 предоставляет методы для регистрации рабочих элементов: Dispatcher.InvokeAsync, Dispatcher.BeginInvokeи Dispatcher.Invoke. Эти методы запланируйте делегат для выполнения. Invoke является синхронным вызовом, то есть он не возвращается, пока поток пользовательского интерфейса фактически завершит выполнение делегата. InvokeAsync и BeginInvoke являются асинхронными и возвращаются немедленно.

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

Однопоточное приложение с длительным вычислением

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

Пример приложения, демонстрирующего основные понятия этого раздела, можно скачать с GitHub для C# или Visual Basic.

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

Screenshot that shows threading of prime numbers.

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

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

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

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

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

Screenshot that shows the dispatcher queue.

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

В следующем примере показан код XAML, который создает пользовательский интерфейс.

Важно!

XAML, показанный в этой статье, находится в проекте C#. Visual Basic XAML немного отличается при объявлении резервного класса для XAML.

<Window x:Class="SDKSamples.PrimeNumber"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Prime Numbers" Width="360" Height="100">
    <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
        <Button Content="Start"  
                Click="StartStopButton_Click"
                Name="StartStopButton"
                Margin="5,0,5,0" Padding="10,0" />
        
        <TextBlock Margin="10,0,0,0">Biggest Prime Found:</TextBlock>
        <TextBlock Name="bigPrime" Margin="4,0,0,0">3</TextBlock>
    </StackPanel>
</Window>

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

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

namespace SDKSamples
{
    public partial class PrimeNumber : Window
    {
        // Current number to check
        private long _num = 3;
        private bool _runCalculation = false;

        public PrimeNumber() =>
            InitializeComponent();

        private void StartStopButton_Click(object sender, RoutedEventArgs e)
        {
            _runCalculation = !_runCalculation;

            if (_runCalculation)
            {
                StartStopButton.Content = "Stop";
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
            }
            else
                StartStopButton.Content = "Resume";
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            _isPrime = true;

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

            // If a prime number, update the UI text
            if (_isPrime)
                bigPrime.Text = _num.ToString();

            _num += 2;
            
            // Requeue this method on the dispatcher
            if (_runCalculation)
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
        }

        private bool _isPrime = false;
    }
}
Imports System.Windows.Threading

Public Class PrimeNumber
    ' Current number to check
    Private _num As Long = 3
    Private _runCalculation As Boolean = False

    Private Sub StartStopButton_Click(sender As Object, e As RoutedEventArgs)
        _runCalculation = Not _runCalculation

        If _runCalculation Then
            StartStopButton.Content = "Stop"
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        Else
            StartStopButton.Content = "Resume"
        End If

    End Sub

    Public Sub CheckNextNumber()
        ' Reset flag.
        _isPrime = True

        For i As Long = 3 To Math.Sqrt(_num)
            If (_num Mod i = 0) Then

                ' Set Not a prime flag to true.
                _isPrime = False
                Exit For
            End If
        Next

        ' If a prime number, update the UI text
        If _isPrime Then
            bigPrime.Text = _num.ToString()
        End If

        _num += 2

        ' Requeue this method on the dispatcher
        If (_runCalculation) Then
            StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
        End If
    End Sub

    Private _isPrime As Boolean
End Class

Помимо обновления текста в ButtonStartStopButton_Click обработчике отвечает за планирование первого простого номера проверка путем добавления делегата в Dispatcher очередь. Через некоторое время после завершения работы Dispatcher обработчика событий будет выбран делегат для выполнения.

Как упоминалось ранее, InvokeAsync является членом Dispatcher, который размещает делегат для выполнения. В данном сценарии выберем приоритет SystemIdle. Объект Dispatcher будет выполнять данный делегат только при отсутствии важных событий для обработки. Быстродействие пользовательского интерфейса представляет большую важность, чем проверка числа. Также передается новый делегат, представляющий подпрограмму проверки числа.

public void CheckNextNumber()
{
    // Reset flag.
    _isPrime = true;

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

    // If a prime number, update the UI text
    if (_isPrime)
        bigPrime.Text = _num.ToString();

    _num += 2;
    
    // Requeue this method on the dispatcher
    if (_runCalculation)
        StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}

private bool _isPrime = false;
Public Sub CheckNextNumber()
    ' Reset flag.
    _isPrime = True

    For i As Long = 3 To Math.Sqrt(_num)
        If (_num Mod i = 0) Then

            ' Set Not a prime flag to true.
            _isPrime = False
            Exit For
        End If
    Next

    ' If a prime number, update the UI text
    If _isPrime Then
        bigPrime.Text = _num.ToString()
    End If

    _num += 2

    ' Requeue this method on the dispatcher
    If (_runCalculation) Then
        StartStopButton.Dispatcher.InvokeAsync(AddressOf CheckNextNumber, DispatcherPriority.SystemIdle)
    End If
End Sub

Private _isPrime As Boolean

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

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

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

Таким образом работает проводник Windows. Каждое новое окно Обозреватель принадлежит исходному процессу, но оно создается под контролем независимого потока. Если Обозреватель становится неответственным, например при поиске сетевых ресурсов, другие Обозреватель окна продолжают реагировать и использовать их.

Мы можем продемонстрировать эту концепцию с помощью следующего примера.

A screenshot of a WPF window that's duplicated four times. Three of the windows indicate that they're using the same thread, while the other two are on different threads.

Первые три окна этого образа используют один и тот же идентификатор потока: 1. Два других окна имеют разные идентификаторы потоков: Девять и 4. В правом верхнем углу каждого окна имеется цветной !️ глиф!

В этом примере содержится окно с поворотным ‼️ глифом , кнопкой приостановки и двумя другими кнопками, которые создают новое окно под текущим потоком или в новом потоке. ‼️ Глиф постоянно поворачивается до нажатия кнопки "Пауза", которая приостанавливает поток в течение пяти секунд. В нижней части окна отображается идентификатор потока.

При нажатии кнопки "Пауза" все окна под тем же потоком становятся неответственными. Любое окно под другим потоком продолжает работать нормально.

Следующий пример — XAML в окне:

<Window x:Class="SDKSamples.MultiWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Thread Hosted Window" Width="360" Height="180" SizeToContent="Height" ResizeMode="NoResize" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBlock HorizontalAlignment="Right" Margin="30,0" Text="‼️" FontSize="50" FontWeight="ExtraBold"
                   Foreground="Magenta" RenderTransformOrigin="0.5,0.5" Name="RotatedTextBlock">
            <TextBlock.RenderTransform>
                <RotateTransform Angle="0" />
            </TextBlock.RenderTransform>
            <TextBlock.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="RotatedTextBlock"
                                Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
                                From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </TextBlock.Triggers>
        </TextBlock>

        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
            <Button Content="Pause" Click="PauseButton_Click" Margin="5,0" Padding="10,0" />
            <TextBlock Margin="5,0,0,0" Text="<-- Pause for 5 seconds" />
        </StackPanel>

        <StackPanel Grid.Row="1" Margin="10">
            <Button Content="Create 'Same Thread' Window" Click="SameThreadWindow_Click" />
            <Button Content="Create 'New Thread' Window" Click="NewThreadWindow_Click" Margin="0,10,0,0" />
        </StackPanel>

        <StatusBar Grid.Row="2" VerticalAlignment="Bottom">
            <StatusBarItem Content="Thread ID" Name="ThreadStatusItem" />
        </StatusBar>

    </Grid>
</Window>

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

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace SDKSamples
{
    public partial class MultiWindow : Window
    {
        public MultiWindow() =>
            InitializeComponent();

        private void Window_Loaded(object sender, RoutedEventArgs e) =>
            ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}";

        private void PauseButton_Click(object sender, RoutedEventArgs e) =>
            Task.Delay(TimeSpan.FromSeconds(5)).Wait();

        private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
            new MultiWindow().Show();

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

        private void ThreadStartingPoint()
        {
            new MultiWindow().Show();

            System.Windows.Threading.Dispatcher.Run();
        }
    }
}
Imports System.Threading

Public Class MultiWindow
    Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
        ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}"
    End Sub

    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub

    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub

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

    Private Sub ThreadStartingPoint()
        Dim window As New MultiWindow()
        window.Show()

        System.Windows.Threading.Dispatcher.Run()
    End Sub
End Class

Ниже приведены некоторые сведения, которые необходимо отметить:

  • Задача Task.Delay(TimeSpan) используется для приостановки текущего потока в течение пяти секунд при нажатии кнопки "Пауза ".

    private void PauseButton_Click(object sender, RoutedEventArgs e) =>
        Task.Delay(TimeSpan.FromSeconds(5)).Wait();
    
    Private Sub PauseButton_Click(sender As Object, e As RoutedEventArgs)
        Task.Delay(TimeSpan.FromSeconds(5)).Wait()
    End Sub
    
  • Обработчик SameThreadWindow_Click событий неявно отображает новое окно в текущем потоке. Обработчик NewThreadWindow_Click событий создает новый поток, который запускает выполнение ThreadStartingPoint метода, который в свою очередь показывает новое окно, как описано в следующей точке маркера.

    private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
        new MultiWindow().Show();
    
    private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
    {
        Thread newWindowThread = new Thread(ThreadStartingPoint);
        newWindowThread.SetApartmentState(ApartmentState.STA);
        newWindowThread.IsBackground = true;
        newWindowThread.Start();
    }
    
    Private Sub SameThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim window As New MultiWindow()
        window.Show()
    End Sub
    
    Private Sub NewThreadWindow_Click(sender As Object, e As RoutedEventArgs)
        Dim newWindowThread = New Thread(AddressOf ThreadStartingPoint)
        newWindowThread.SetApartmentState(ApartmentState.STA)
        newWindowThread.IsBackground = True
        newWindowThread.Start()
    End Sub
    
  • Метод ThreadStartingPoint является отправной точкой для нового потока. Новое окно создается под контролем этого потока. WPF автоматически создает новый объект System.Windows.Threading.Dispatcher для управления новым потоком. Все, что нужно сделать для обеспечения функциональности окна — это запустить System.Windows.Threading.Dispatcher.

    private void ThreadStartingPoint()
    {
        new MultiWindow().Show();
    
        System.Windows.Threading.Dispatcher.Run();
    }
    
    Private Sub ThreadStartingPoint()
        Dim window As New MultiWindow()
        window.Show()
    
        System.Windows.Threading.Dispatcher.Run()
    End Sub
    

Пример приложения, демонстрирующего основные понятия этого раздела, можно скачать с GitHub для C# или Visual Basic.

Обработка операции блокировки с помощью Task.Run

Обработка блокировки операций в графическом приложении может оказаться трудной задачей. Мы не хотим вызывать методы блокировки от обработчиков событий, так как приложение, как представляется, зависает. Предыдущий пример создал новые окна в собственном потоке, позволяя каждому окну работать независимо друг от друга. Хотя мы можем создать новый поток с System.Windows.Threading.Dispatcherпомощью, после завершения работы сложно синхронизировать новый поток с основным потоком пользовательского интерфейса. Так как новый поток не может напрямую изменить пользовательский интерфейс, необходимо использовать Dispatcher.InvokeAsync( Dispatcher.BeginInvokeили Dispatcher.Invoke) для вставки делегатов в Dispatcher поток пользовательского интерфейса. В конечном итоге эти делегаты выполняются с разрешением на изменение элементов пользовательского интерфейса.

Существует более простой способ запуска кода в новом потоке при синхронизации результатов, асинхронного шаблона на основе задач (TAP). Он основан на Task типах в Task<TResult>System.Threading.Tasks пространстве имен, которые используются для представления асинхронных операций. TAP использует один метод для представления инициализации и завершения асинхронной операции. Существует несколько преимуществ для этого шаблона:

  • Вызывающий Task объект может выполнять код асинхронно или синхронно.
  • Ход выполнения можно сообщить из .Task
  • Вызывающий код может приостановить выполнение и ждать результата операции.

Пример Task.Run

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

A diagram that demonstrates the workflow of the example app.

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

Пример приложения, демонстрирующего основные понятия этого раздела, можно скачать с GitHub для C# или Visual Basic. Код XAML для этого примера довольно большой и не указан в этой статье. Используйте предыдущие ссылки GitHub для просмотра XAML. XAML использует одну кнопку для получения погоды.

Рассмотрим код для XAML:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Threading.Tasks;

namespace SDKSamples
{
    public partial class Weather : Window
    {
        public Weather() =>
            InitializeComponent();

        private async void FetchButton_Click(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);

            // Asynchronously fetch the weather forecast on a different thread and pause this code.
            string weather = await Task.Run(FetchWeatherFromServerAsync);

            // After async data returns, process it...
            // Set the weather image
            if (weather == "sunny")
                weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];

            else if (weather == "rainy")
                weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];

            //Stop clock animation
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
            ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
            
            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;
        }

        private async Task<string> FetchWeatherFromServerAsync()
        {
            // Simulate the delay from network access
            await Task.Delay(TimeSpan.FromSeconds(4));

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

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

        private void HideClockFaceStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowWeatherImageStoryboard"]).Begin(ClockImage);

        private void HideWeatherImageStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Begin(ClockImage, true);
    }
}
Imports System.Windows.Media.Animation

Public Class Weather

    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)

        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)

        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)

        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)

        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)

        End If

        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)

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

    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)

        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))

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

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

    End Function

    Private Sub HideClockFaceStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowWeatherImageStoryboard"), Storyboard).Begin(ClockImage)
    End Sub

    Private Sub HideWeatherImageStoryboard_Completed(sender As Object, e As EventArgs)
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Begin(ClockImage, True)
    End Sub
End Class

Ниже приведены некоторые подробности, на которые следует обратить внимание.

  • Обработчик событий кнопки

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    Обратите внимание, что обработчик событий был объявлен с async (или Async с Visual Basic). Метод async позволяет приостановить код при вызове ожидаемого метода, например FetchWeatherFromServerAsync. Это обозначается await ключевое слово (или Await с помощью Visual Basic). FetchWeatherFromServerAsync Пока не завершится, код обработчика кнопки приостановлен и элемент управления возвращается вызывающей объекту. Это аналогично синхронному методу, за исключением того, что синхронный метод ожидает завершения каждой операции в методе, после чего элемент управления возвращается вызывающему объекту.

    Ожидаемые методы используют контекст потоков текущего метода, который с обработчиком кнопки является потоком пользовательского интерфейса. Это означает, что вызов await FetchWeatherFromServerAsync(); (или Await FetchWeatherFromServerAsync() visual Basic) приводит к выполнению кода в FetchWeatherFromServerAsync потоке пользовательского интерфейса, но не выполняется на диспетчере время его выполнения, аналогично тому, как работает однопоточное приложение с длительным примером вычисления . Однако обратите внимание, что await Task.Run используется. При этом создается новый поток в пуле потоков для указанной задачи вместо текущего потока. Поэтому FetchWeatherFromServerAsync выполняется в собственном потоке.

  • Выборка погоды

    private async Task<string> FetchWeatherFromServerAsync()
    {
        // Simulate the delay from network access
        await Task.Delay(TimeSpan.FromSeconds(4));
    
        // Tried and true method for weather forecasting - random numbers
        Random rand = new Random();
    
        if (rand.Next(2) == 0)
            return "rainy";
        
        else
            return "sunny";
    }
    
    Private Async Function FetchWeatherFromServerAsync() As Task(Of String)
    
        ' Simulate the delay from network access
        Await Task.Delay(TimeSpan.FromSeconds(4))
    
        ' Tried and true method for weather forecasting - random numbers
        Dim rand As New Random()
    
        If rand.Next(2) = 0 Then
            Return "rainy"
        Else
            Return "sunny"
        End If
    
    End Function
    

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

    Task.Delay По завершении, и мы случайно выбрали наш прогноз погоды, состояние погоды возвращается вызывающей.

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

    private async void FetchButton_Click(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);
    
        // Asynchronously fetch the weather forecast on a different thread and pause this code.
        string weather = await Task.Run(FetchWeatherFromServerAsync);
    
        // After async data returns, process it...
        // Set the weather image
        if (weather == "sunny")
            weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];
    
        else if (weather == "rainy")
            weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];
    
        //Stop clock animation
        ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
        ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
        
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;
    }
    
    Private Async Sub FetchButton_Click(sender As Object, e As RoutedEventArgs)
    
        ' Change the status image and start the rotation animation.
        fetchButton.IsEnabled = False
        fetchButton.Content = "Contacting Server"
        weatherText.Text = ""
        DirectCast(Resources("HideWeatherImageStoryboard"), Storyboard).Begin(Me)
    
        ' Asynchronously fetch the weather forecast on a different thread and pause this code.
        Dim weatherType As String = Await Task.Run(AddressOf FetchWeatherFromServerAsync)
    
        ' After async data returns, process it...
        ' Set the weather image
        If weatherType = "sunny" Then
            weatherIndicatorImage.Source = DirectCast(Resources("SunnyImageSource"), ImageSource)
    
        ElseIf weatherType = "rainy" Then
            weatherIndicatorImage.Source = DirectCast(Resources("RainingImageSource"), ImageSource)
    
        End If
    
        ' Stop clock animation
        DirectCast(Resources("ShowClockFaceStoryboard"), Storyboard).Stop(ClockImage)
        DirectCast(Resources("HideClockFaceStoryboard"), Storyboard).Begin(ClockImage)
    
        ' Update UI text
        fetchButton.IsEnabled = True
        fetchButton.Content = "Fetch Forecast"
        weatherText.Text = weatherType
    End Sub
    

    Когда задача завершится, и поток пользовательского Task.Runинтерфейса имеет время, вызывающий обработчик событий кнопки, возобновляется. Остальная часть метода останавливает анимацию часов и выбирает изображение для описания погоды. Он отображает это изображение и включает кнопку "Получить прогноз".

Пример приложения, демонстрирующего основные понятия этого раздела, можно скачать с GitHub для C# или Visual Basic.

Технические сведения и точки спотыкания

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

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

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

Screenshot that shows a MessageBox with an OK button

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

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

Diagram that shows threading reentrancy.

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

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

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

См. также