Using system windows threading

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

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

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

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

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

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

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

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

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

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

Снимок экрана, показывающий threading простых чисел.

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

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

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

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

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

Снимок экрана, показывающий очереди диспетчера.

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

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

<Window x:Class="SDKSamples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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 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 void StartOrStop(object sender, EventArgs e)
{
    if (continueCalculating)
    {
        continueCalculating = false;
        startStopButton.Content = "Resume";
    }
    else
    {
        continueCalculating = true;
        startStopButton.Content = "Stop";
        startStopButton.Dispatcher.BeginInvoke(
            DispatcherPriority.Normal,
            new NextPrimeDelegate(CheckNextNumber));
    }
}

Помимо обновления текста в Button, этот обработчик отвечает за планирование проверки первого простого числа путем добавления делегата к Dispatcher очереди. Иногда после завершения работы, этот обработчик событий Dispatcher выберет этот делегат для выполнения.

Как было упомянуто ранее, BeginInvoke является Dispatcher членом, который используется при планировании делегата для выполнения. В этом случае мы выбираем SystemIdle приоритет. Dispatcher Будет выполнять данный делегат только в том случае, если отсутствуют важные события для обработки. UI скорость реагирования важнее, чем проверка числа. Также передается новый делегат, представляющий подпрограмму проверки числа.

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

    for (long i = 3; i <= Math.Sqrt(num); i++)
    {
        if (num % i == 0)
        {
            // Set not a prime flag to 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;

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

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

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

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

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

Снимок экрана, показывающий погоды пользовательского интерфейса.

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

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

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

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

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

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

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

            fetcher.BeginInvoke(null, null);
        }

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

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

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

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

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

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

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

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

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

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

  • Создание обработчика кнопки

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

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

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

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

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

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

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

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

Когда Dispatcher в UI времени у потока, он выполняет запланированный вызов метода UpdateUserInterface. Этот метод останавливает анимацию часов и выбирает изображение для описания погоды. Он отображает это изображение и восстанавливает кнопку «Получить прогноз погоды».

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

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

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

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

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

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

<Window x:Class="SDKSamples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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("http://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 использовать один из методов, описанных выше, таких как создание фонового потока, для работы в асинхронном режиме, не блокируя вызывающий поток.

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

DispatcherSynchronizationContext Класс адреса этой задачи — представляйте его упрощенную версию Dispatcher , работает с другими UI также платформ.

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

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

Схема, показывает, работа с потоками повторный вход.

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

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

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

См. также

  • Однопоточного приложения с образцом выполняющейся длительное время вычисления

To explain what a thread is let’s first start off with the explanation of what a process is. A process is an instance of your application. The OS will assign it a chunk of virtual memory, execution context(program counter, registers, PID, …) and resource handles. One process can have multiple child processes, your web browser is a good example of this:

Usually, the code we write executes procedurally(line by line). So if we have a time-consuming task like writing to a file(which can be very slow compared to the speed at which things happen in the CPU) our program will have to wait for that operation to be over before continuing with its execution. 

You can solve this problem by making a new child thread that will branch off the main one and run in parallel. This way the main thread can continue to run and the time-consuming operation will be executing in the child thread and not holding up the main one. I should also note that it makes sense to use a new thread only for CPU bound operations and not for I/O bound ones. See this post I made for more information on the topic.

By default, threads are started as foreground threads. If you want to make a background thread you must set the IsBackground property to true before starting it. The difference between these two is that the foreground threads will keep running until they are done even if the parent thread was terminated. Meanwhile, a background thread will terminate when its parent thread is terminated even if it still has work to do.

Note: 

Most of the time the threads don’t actually run in parallel. Each one gets to run for some time after that CPU time is given to another one. It all just happens so fast it appears as if things are running in parallel. 

If you open up your task manager and look at the number of threads running you will see that you are probably running thousands of threads at once and you probably have something like 4-16 physical cores(and 8-32 virtual cores if your CPU supports hyperthreading). If we hand as many CPU cores as we have threads than all our threads could truly run in parallel. But it this case only 4-32 threads will actually run-in parallel.

Let’s have a look at the code below to see how to use threads.

Posted by: Zeeshan Amjad | July 20, 2009

WPF Application starts with two threads. One for rendering that runs in background and other to manage the user interface. It is UI thread that is responsible for user input, handle events and draw everything on the screen. In WPF almost all of the objects belong to UI thread.

UI thread queues every work item along with its priority into the Dispatcher. Now it is Dispatcher who selects the work items one by one based on its priority. Every UI thread must have at least one Dispatcher. In addition each Dispatcher executes the work order in one thread only. If we try to access the user interface element that is created by another thread then we will get an exception. Here is a simple program to demonstrate this.

  1: using System;
  2: using System.Windows;
  3: using System.Threading;
  4: 
  5: public class wpf05
  6: {
  7:     static void ThreadFun(object obj)
  8:     {
  9:         Window win = (Window)obj;
 10:         if (win != null)
 11:         {
 12:             win.Title = "Writing from Thread";
 13:         }
 14:     }
 15: 
 16:     [STAThread]
 17:     public static void Main()
 18:     {
 19:         Window win = new Window();
 20:         win.Title = "Hello World";
 21: 
 22:         Thread th = new Thread(new ParameterizedThreadStart(ThreadFun));
 23:         th.Start(win);
 24: 
 25:         Application app = new Application();
 26:         app.Run(win);
 27:     }
 28: }
 29: 

This program will throw an InvalidOperationException because we are trying to modify the Window property from another thread. Also note that here we are also using System::Threading namespace.

Here is the simplest way to avoid this exception. But this program is not doing our required task.

  1: using System;
  2: using System.Windows;
  3: using System.Threading;
  4: using System.Windows.Threading;
  5: 
  6: public class wpf 
  7: {
  8:     static void ThreadFun(object obj)
  9:     {
 10:         Window win = (Window)obj;
 11:         if (win != null)
 12:         {
 13:             if (win.CheckAccess())
 14:             {
 15:                 win.Title = "Writing from Thread";
 16:             }
 17:         }
 18:     }
 19: 
 20:     [STAThread]
 21:     public static void Main()
 22:     {
 23:         Window win = new Window();
 24:         win.Title = "Hello World";
 25: 
 26:         Thread th = new Thread(new ParameterizedThreadStart(ThreadFun));
 27:         th.Start(win);
 28: 
 29:         Application app = new Application();
 30:         app.Run(win);
 31:     }
 32: }

In WPF only one thread can update the UI. If any other wants to update the user interface then it will request by using the Dispatcher class to perform that operation. Here is the proper way to update the User interface element from another thread.

  1: using System;
  2: using System.Windows;
  3: using System.Threading;
  4: using System.Windows.Threading;
  5: 
  6: public class MyWindow : Window
  7: {
  8:     private delegate void MyDelegate(String str);
  9: 
 10:     public MyWindow()
 11:     {        
 12:         Title = "Hello World";
 13:     }
 14: 
 15:     public void SetTitle(String title)
 16:     {
 17:         Thread.Sleep(3000);
 18:         Title = title;
 19:     }
 20: 
 21:     public void ThreadFun()
 22:     {
 23:         MyDelegate setTitle = SetTitle;
 24:         Dispatcher.BeginInvoke(DispatcherPriority.Normal,
 25:             setTitle, "Hello World from Thread");
 26:     }
 27: 
 28:     public void StartThread()
 29:     {
 30:         Thread th = new Thread(ThreadFun);
 31:         th.Start();
 32:     }
 33: }
 34: 
 35: public class wpf 
 36: {
 37: 
 38:     [STAThread]
 39:     public static void Main()
 40:     {
 41:         MyWindow win = new MyWindow();
 42: 
 43:         win.StartThread();
 44: 
 45:         Application app = new Application();
 46:         app.Run(win);
 47:     }
 48: }
 49: 

This program will change the title of main window from “Hello World” to “Hello World from Thread”.

We don’t have to create a delegate and instance of it for such a small work. We can also take an advantage of C# 2008 and create anonymous method for the delegate. Here is the same program with anonymous method.

  1: using System;
  2: using System.Windows;
  3: using System.Threading;
  4: using System.Windows.Threading;
  5: 
  6: public class MyWindow : Window
  7: {
  8:     public MyWindow()
  9:     {        
 10:         Title = "Hello World";
 11:     }
 12: 
 13:     public void ThreadFun()
 14:     {
 15:         Dispatcher.BeginInvoke(DispatcherPriority.Normal,
 16:             (ThreadStart) delegate 
 17:                 {
 18:                     Thread.Sleep(3000);
 19:                     Title = "Hello World from Thread"; 
 20:                 }
 21:             );
 22:     }
 23: 
 24:     public void StartThread()
 25:     {
 26:         Thread th = new Thread(ThreadFun);
 27:         th.Start();
 28:     }
 29: }
 30: 
 31: public class wpf 
 32: {
 33: 
 34:     [STAThread]
 35:     public static void Main()
 36:     {
 37:         MyWindow win = new MyWindow();
 38: 
 39:         win.StartThread();
 40: 
 41:         Application app = new Application();
 42:         app.Run(win);
 43:     }
 44: }
 45: 

Ways Of Creating Multithreaded Applications In .NET (Part 1). What .NET Threads Are

With the advent of multi-core processors, multithreading has become almost indispensable in the development of applications. It is multi-threading that gives significant performance gain when using multiple processor cores.

However, multithreading comes with a lot of hidden pitfalls that are very unpleasant for inexperienced developers. Therefore, we have decided to make a series of articles devoted to multithreading methods in .NET applications using the C# language as an example.

In the first part, we’ll talk about multitasking and multithreading, we’ll consider the architecture of multi-core processors and how processor cores are arranged in an operating system. We’ll also review operating system tools for creating multithreaded applications, and we’ll take a closer look at the Thread class.

What is multitasking and multithreading?

Multitasking has become quite a natural phenomenon in modern operating systems. When several applications are running at the same time, the operating system can quickly switch between them, giving them CPU processing resources in turns. This creates the illusion that several programs are running simultaneously. This separation seems inconspicuous since neither the person nor the fastest Internet connection can work at the speeds with which modern processors process information.

Multithreading is the same thing as multitasking but within one application. The operating system switches between different parts of the same application quickly, thus creating the illusion that it is executing them simultaneously.

If you’re interested in more, read Microsoft Roslyn – using the compiler as a service

Architecture of modern computers

In the 2000s, when the CPU clock speeds grew rapidly, it seemed that nothing could stop this growth. Some experts predicted that the 10 GHz mark will be exceeded by 2010. This growth was proportional to reduction in the size of processor transistors, while an increase in processor power in those times was significantly ahead of Gordon Moore’s predictions (see Moore’s law).

However, before long, engineers encountered problems – substantial increase in heat release and fundamental limitations on transistor size. As a result, further increase in performance by improving CPU clock speed became practically impossible, and the clock speed remained at the 3-5 GHz mark today.

Engineers had to look for other ways of improving CPU performance. They found such an effective solution in multiprocessor information processing. If you cannot make the processor faster, why not add one more processor? In this case, you don’t need to create such a processor in the form of a separate device. The easiest way is to create such processors within one module so that they all have equal access to shared memory. Such processors were called physical processor cores.

Logical processor cores and hyper-threading

Operating systems operate on logical processor cores, sharing the time resources of each of the processor cores between processes and threads. A logical processor core may not always match with a physical processor.

In 2002, the Intel Pentium 4 processor introduced the Hyper-Threading technology for execution of commands. Hyper-Threading involves execution of multiple command threads by one physical processor core. In this case, the operating system sees each thread as a separate logical core. Dual-threaded hyper-threading works by adding another set of registers, an instruction pointer, and an interrupt controller into the physical core of the processor. Here, the number and set of execution units in the core remains unchanged.

Hyper-threading appeared as a solution to the problem of frequent downtime of the computational pipeline of Intel Pentium 4 processor, associated with excessive increase in the number of information processing stages in this pipeline.

The reasons for such downtime were:

  • The branching instruction was incorrectly predicted when executing conditional and unconditional branches.
  • There was a miss when accessing the processor’s cache and data needed to be loaded into the cache from the RAM.
  • The result of the previous instruction, which is still executing, is needed to execute the next instruction.

It should be understood that hyper-threading threads are not full-fledged physical processor cores, so they do not give multiple increase in performance. On average, the performance gain from hyper-threading is 1-30%, depending on the task being solved. In some tasks, there can be no performance increase at all. Nevertheless, hyper-threading is used in processors to this day, for example, in Intel Core i3, Core i7, Atom, Pentium, AMD Ryzen, and others.

If you’re interested in more, read .NET Core Framework Complete Review

Processes and threads in operating systems

The operating system works with logical processor cores, not knowing about their physical implementation. It sees the physical processor cores and hyper-threading threads as the same. A clear example of this is a screenshot of Windows Task Manager for a quad-core (4 physical cores + 4 hyper-threading threads) of the Intel Core i7-4770K processor in Windows 7 (Fig. 1).

Fig. 1 – Screenshot of the Windows Task Manager for Core i7-4770K.

The main program object of an operating system is the process. A process is an executable instance of an application that owns system resources (for example, RAM resources or I/O threads).

Each process can have one or more threads. Each thread executes part of the process code and has its own stack and registers. Threads can only access process resources and share them among themselves. The structure of a multithreaded program is shown in Fig. 2.

Fig. 2 – Structure of a single-threaded and multithreaded program.

At the same time, it is much faster to switch between threads than between processes during execution. Therefore, in terms of computing resources, it’s more profitable working with threads than working with processes. In addition, threads are supported by most operating systems and software platforms, for example:

  • WIN32 API Threads (Windows)
  • Cocoa Threads (iOS)
  • Multiprocessing Services (iOS)
  • Java Threads (Android)
  • POSIX Threads (GNU/Linux)
  • C Runtime Library (C)
  • OpenMP (C++, Fortran)
  • Intel Threading Building Blocks (C++, Fortran)

We will consider the technology for working with threads in .NET using C# as an example.

Thread class

The System.Threading namespace contains all the tools for low-level thread creation and management. First, we add this namespace to the project.

using System.Threading;

If threads have not yet been created, then at least one thread is already executing in the application. Let’s call it Main. To create another thread, we need to create a new Thread object.

Thread t1 = new Thread(GetThreadld);

In this case, the constructor of this object must pass the name of the function whose code will be executed in this thread. Here, it is the GetThreadId function. This function is passed as an object (in other words, a delegate) to the constructor’s argument. In this case, a function represented as a delegate has neither parameters nor return value. However, we can get out of this situation. For example, we can give a command to start a thread and simultaneously pass function parameters.

t1.Start("1");

Until the Start command is given, the thread will not start execution. In this case, the GetThreadId function can pass only an object as a parameter. The GetThreadId itself is declared as:

static void GetThreadld(object data)

A thread can be assigned a priority – both before its start and during execution.

t1.Priority = ThreadPriority.Lowest;
t2.Priority = ThreadPriority.BelowNormal;
t3.Priority = ThreadPriority.Normal;
t4.Priority = ThreadPriority.Highest;

ThreadPriority is a listed type here.

After all the threads have started, you need to call a function that is waiting for completion of their work. This is the Join function. Once the Join for all threads is executed, the threads will be terminated and destroyed. .NET automatically frees any resources that were occupied by these threads.

//waiting for all threads to finish executing
t1.Join();
t2.Join();
t3.Join();
t4.Join();

After the threads have finished executing, the main thread will again take over code execution completely.

Example of how the Thread class works

The following example displays 1000 messages from different threads with different priorities. Threads write their numbers to the console 1000 times each.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{
class Program
{
static void GetThreadld(object data)
{
// now the Main thread will display the received string (its number) one thousand times
for(int i = 0; i <= 1000; i++)
Console.Write(data);
}

static void Main(string[] args)
{
// we create 4 threads, we transfer as parameter the name of the function executed by the thread
Thread t1 = new Thread(GetThreadld);
Thread t2 = new Thread(GetThreadld);
Thread t3 = new Thread(GetThreadld);
Thread t4 = new Thread(GetThreadld);

// we assign priorities to threads
t1.Priority = ThreadPriority.Lowest; // lowest
t2.Priority = ThreadPriority.BelowNormal; // below normal
t3.Priority = ThreadPriority.Normal; // normal
t4.Priority = ThreadPriority.Highest; // highest

// we run each thread and pass the thread number as a parameter
t1.Start("1");
t2.Start("2");
t3.Start("3");
t4.Start("4");

Console.WriteLine("all threads have started");
// waiting for all threads to finish executing
t1.Join();
t2.Join();
t3.Join();
t4.Join();

Console.ReadKey(); // Until the user presses the key, the program will not end (so that you will have time to view the result)
}
}
}

The program execution result is shown in Fig. 3.

Fig. 3 – Visual illustration of the work of threads with different priorities on a quad-core processor.

The example in Fig. 3 shows three facts:

  1. Creation of threads is a fairly time-consuming operation. First, thread 1 with the lowest priority was created and immediately launched for execution. Then the other threads were created and launched in turns.
  2. Threads are terminated according to their priority: the thread with the highest priority (4) ended earlier than the other threads.
  3. If a multithreaded program is running on a multi-core processor, the priority of threads becomes less significant here than on single-core processors, since threads will be allocated among all the processor cores.

Background and foreground threads

Threads can be foreground and background. The difference between foreground threads is that the program does not end until all the foreground threads have been executed. Background threads do not hinder the completion of a program and are terminated together with it, even if the process that the background threads were executing have not yet been run.

To find out whether a thread is a background or foreground thread, use the IsBackground property.

bool bg = Thread.CurrentThread.IsBackground;

where CurrentThread is a static method of the Thread class, which returns a link to the thread that is currently an active thread.

By default, all threads are created by foreground threads. But anywhere in the program code, you can make the thread to become background and vice versa.

t2.IsBackground = true;

Possible errors when working with the Thread class

Despite the simplicity of working with the Thread class, many novice developers make gross errors when creating multi-threaded applications.

Error 1

The most common mistake made by inexperienced developers is that they try to catch exceptions that occur in child threads, using the try { } catch block to wrap its call from the parent thread. The point is that in this case, all exceptions in the parent thread will be processed, while exceptions in child threads will remain unprocessed and lead to immediate termination of the application. The listing below shows how not to catch exceptions in multithreaded applications.

try
{ // This code is incorrect
t1.Start("1" );
t2.Start("2" );
t3.Start("3" );
t4.Start("4" );
t2.IsBackground = true;
t3.IsBackground = true;
t4.IsBackground = true;
Console.WriteLine(" all threads have started ");
//waiting for all threads to finish executing
t1.Join();
t2.Join();
t3.Join();
t4.Join();
} catch (Exception e)
{ // only exceptions in the parent thread will be processed here
Console.WriteLine(e.ToString());
} // exceptions in child threads will not be processed and they will stop the application

To catch all exceptions in child threads, the try {} catch block must be located inside the function that will be passed to the child thread for execution, as in the listing below:

static void threadID(object data)
{
try
{
// now the thread will display the received string (its number) one thousand times
for (int i = 0; i <= 1000; i++)
Console.Write(data);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}

Error 2

The second error is the attempt to access the application interface from the child thread. When developing applications with a graphical user interface (for example, WinForms or WPF applications), there is always a main thread that monitors the state of the GUI elements. Only this thread can change the state of the interface elements. Any other thread, when accessing these elements, will immediately throw an exception.

In WinForms applications, the compiler will mark as erroneous the code that accessed the controls from another thread. In additional information, the following will be written about this error:

“Additional information: Cross-thread operation not valid: Control ‘textBox1’ accessed from a thread other than the thread it was created on”.

In WPF applications, the situation is even worse. This will throw up an InvalidOperationException exception during execution of the application with the description “The calling thread cannot access this object because its owner is another thread” (Fig. 4).

Fig. 4 – An example of an exception when accessing the elements of the WPF application interface from another thread.

If there is still a need to change the interface elements, there are fairly simple solutions in this case. If you need to access the interface in a WinForms application, you will need to first perform a check by calling the InvokeRequired method from the interface element. If the InvokeRequired condition is true, then execute the Invoke method (see the listing below). If Invoke has already worked, then the interface element can be accessed directly.

void ControlAccess()
{
if(textBox1.InvokeRequired)
textBox1.Invoke(ControlAccess);
else
textBox1.Text = "тест";
}

Calling Invoke without InvokeRequired check will also throw an exception.

In WPF applications, special object Dispatcher is used to access interface objects from other threads. To organize a call, you need to connect the following System.Windows.Threading space.

using System.Windows.Threading;

Next, we need to wrap the application interface from another thread with the static Invoke method of the Dispatcher class.

this.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate() { Cons.Text = "Industrial";});

Here “this” is a pointer to the current window whose interface elements are accessed. DispatcherPriority is an enumerated type that is responsible for interface access priority. Type gradation is exactly the same as when creating threads.

With the delegate() function, we declare an anonymous function and pass code to it with direct access to the interface elements. After declaring the anonymous function, it must be cast to the ThreadStart type (casting to a type is indicated in parentheses before the variable or function). In this case, accessing the interface will not throw any exceptions in the WPF application.

Error 3

A fairly common mistake is the lack of control over the completion of threads. The point is that the .NET Common Language Runtime (CLR) environment does not know if the thread will continue to perform any actions after it has completed all the work. Therefore, all responsibilities for completing the work of threads and their destruction lie on the shoulders of the developer.

The developer is obliged to ensure correct completion of all application threads in case the application was closed (including abnormally). If this is not done, the application’s parent thread will be terminated, while the child threads will continue to execute (even when the application window is already closed). At the same time, they will consume system resources, and after closing the application window, you can stop them only through the task manager.

This can lead to amusing situations. For example, the author of this article used to study the multimedia capabilities of WPF and worked with the MediaPlayer class, which can open and play *.mp3 files. He did this in a separate thread. If you don’t take care of the ending of the thread that plays music, then even after closing the application window, the music will continue playing.

Conclusion

Modern multi-core processors are designed such as to execute a large number of processes and threads at the same time. Nevertheless, this operation is quite resource-intensive.

Despite all the simplicity and efficiency of working with the Thread class, multithreaded programming is fraught with a lot of dirty tricks, and the developer risks running into unexpected program behavior if he/she doesn’t know about the tricks.

In the following parts of this article, a thread pool that saves significantly on the computing cost of creating threads will be considered. Methods of thread synchronization, multi-sequencing of cycles and database queries will also be considered.

[EAP]

Шаблон использования, основанный на асинхронных событиях (event-based asynchronous pattern, EAP), предоставляет простой способ организации многопоточности без необходимости потребителям заботиться о явном запуске или управлении потоками. Это также предоставляет следующие функции:

• Модель согласованной отмены действий (cooperative cancellation model)
• Возможность безопасно обновлять графические элементы управления приложения (controls GUI-интерфейса WPF или Windows Forms), когда рабочий поток завершил свою работу
• Перенаправление исключений в событие завершения (completion event)

EAP только шаблон, поэтому обработка его функций должны быть написана разработчиком приложения. Только несколько классов в Framework следуют этому шаблону, прежде всего BackgroundWorker (см. далее) и WebClient в System.Net. Сущность шаблона в следующем: класс предоставляет семейство членов, которые внутри себя управляют многопоточностью, примерно так (помеченные желтым куски кода показывают код, который составляет часть этого шаблона):

// Эти члены из класса WebClient:
 
public byte[] DownloadData (Uri address);    // Синхронная версия
public void DownloadDataAsync (Uri address);
public void DownloadDataAsync (Uri address, object userToken);
public event DownloadDataCompletedEventHandler DownloadDataCompleted;
 
public void CancelAsync (object userState);  // Отменяет операцию
public bool IsBusy { get; }                  // Показывает, что работа все еще идет

Методы *Async выполняются асинхронно: другими словами, они запускают операцию в другом потоке и затем немедленно делают возврат управления в вызвавший их код. Когда операция завершилась, срабатывает событие *Completed — автоматически вызывая Invoke, если это требуется приложению WPF или Windows Forms. Это событие передает обратно объект аргументов событий, который содержит:

• Флаг, показывающий, была ли отменена операция (пользователем, вызвавшим CancelAsync)
• Объект Error, показывающий выброшенное исключение (если это имело место)
• Объект userToken, если он был предоставлен при вызове метода Async

Здесь показано, как мы можем использовать EAP-члены класса WebClient для загрузки веб-страницы:

var wc = new WebClient();
wc.DownloadStringCompleted += (sender, args) =>
{
   if (args.Cancelled)
      Console.WriteLine ("Отменено");
   else if (args.Error != null)
      Console.WriteLine ("Исключение (exception): " + args.Error.Message);
   else
   {
      Console.WriteLine (args.Result.Length + ": столько символов было загружено");
      // Здесь мы могли бы обновить интерфейс пользователя (UI)...
   }
};
wc.DownloadStringAsync (new Uri ("http://www.linqpad.net"));   // Запуск загрузки

Класс, следующий модели EAP может предоставить дополнительные группы асинхронных методов. Например:

public string DownloadString (Uri address);
public void DownloadStringAsync (Uri address);
public void DownloadStringAsync (Uri address, object userToken);
public event DownloadStringCompletedEventHandler DownloadStringCompleted;

Однако эти методы будут совместно использовать одни и те же члены CancelAsync и IsBusy. Таким образом одновременно может произойти только одна асинхронная операция.

EAP предоставляет возможность экономить на потоках, если внутренняя реализация следует APM (описывается в Главе 23 книжки «C# 4.0 in a Nutshell» []).

В части 5 [7] этой документации мы увидим, как Tasks [4] предоставляют подобные возможности — включая перенаправление исключения, токены продолжения выполнения и отмены (continuations, cancellation tokens) и поддержку контекстов синхронизации. Это делает реализацию EAP менее привлекательной — кроме простых случаев, которые может обеспечить класс BackgroundWorker.

[BackgroundWorker]

BackgroundWorker это вспомогательный класс из пространства имен System.ComponentModel, предназначенный для управления рабочим потоком (потоком, который выполняет какую-то фоновую работу приложения). Его можно считать реализацией EAP общего назначения с предоставлением следующих функций:

• Модель согласованной отмены действий (cooperative cancellation model)
• Возможность безопасно обновлять графические элементы управления приложения (controls GUI-интерфейса WPF или Windows Forms), когда рабочий поток завершил свою работу
• Перенаправление исключений в событие завершения (completion event)
• Протокол для сообщения о текущем статусе выполняемой операции (reporting progress)
• Реализация IComponent, позволяющая перетащить и бросить компонент в форму приложения Visual Studio

BackgroundWorker использует пул потоков (Thread Pool, см. [2]), что означает, что Вы никогда не должны вызывать Abort на потоке BackgroundWorker.

Использование BackgroundWorker. Вот минимальные шаги для применения BackgroundWorker:

1. Инстанцируйте экземпляр BackgroundWorker и определите обработчик события DoWork.
2. Вызовите метод RunWorkerAsync, опционально с аргументом типа object.

После этого все придет в движение. Любой аргумент, переданный в RunWorkerAsync, будет перенаправлен в обработчик события DoWork через свойство события аргумента Argument. Вот пример:

class Program
{
   static BackgroundWorker _bw = new BackgroundWorker();
 
   static void Main()
   {
      _bw.DoWork += bw_DoWork;
      _bw.RunWorkerAsync ("Сообщение для рабочего потока");
      Console.ReadLine();
   }
 
   static void bw_DoWork (object sender, DoWorkEventArgs e)
   {
      // Этот код будет работать в рабочем потоке:
      Console.WriteLine (e.Argument);  // выведется "Сообщение для рабочего потока"
      // Выполнение какой-то емкой по времени вычислительной задачи...
   }
}

У BackgroundWorker есть событие RunWorkerCompleted, которое сработает, когда обработчик события DoWork завершит свою работу. Обработка RunWorkerCompleted не обязательна, однако обычно она нужна, чтобы запросить информацию о любом исключении, которое было выброшено во время выполнения кода DoWork. Кроме того, код в обработчике RunWorkerCompleted может напрямую обновить интерфейс пользователя без явного маршалирования; код внутри обработчика события DoWork обновлять интерфейс пользователя не может.

Чтобы добавить поддержку сообщения о прогрессе выполнения задачи внутри DoWork (progress reporting):

1. Установите свойство WorkerReportsProgress в значение true.
2. Периодически вызывайте ReportProgress из тела обработчика DoWork, передавая ему процентное значение, обозначающее прогресс выполняемой работы. Также опционально можно передать пользовательский объект состояния (user-state object).
3. Напишите код обработчика события ProgressChanged, опрашивая в нем свойство аргумента события ProgressPercentage.
4. Код в обработчике события ProgressChanged выполняется в главном потоке приложения, поэтому он может свободно взаимодействовать с графическим интерфейсом пользователя (UI), т. е. обновлять его — точно так же, как это может делать обработчик RunWorkerCompleted. Типичный пример — обновление полоски прогресса операции (progress bar).

Чтобы добавить поддержку отмены операции рабочего потока (cancellation):

1. Установите свойство WorkerSupportsCancellation в значение true.
2. Периодически проверяйте свойство CancellationPending в коде обработчика события DoWork. Если оно равно true, установите свойство Cancel аргумента в true и выполните возврат из обработчика DoWork. Также рабочий поток, работающий в теле DoWork, может сам установить Cancel и выйти без установленного свойства CancellationPending, если он решит, что работа слишком сложная, и её нельзя завершить.
3. Вызовите CancelAsync для выставления запроса отмены операции рабочего потока.

Вот пример, который реализует все ранее перечисленные функции:

using System;
using System.Threading;
using System.ComponentModel;
 
class Program
{
   static BackgroundWorker _bw;
 
   static void Main()
   {
      _bw = new BackgroundWorker
      {
         WorkerReportsProgress = true,
         WorkerSupportsCancellation = true
      };
      _bw.DoWork += bw_DoWork;
      _bw.ProgressChanged += bw_ProgressChanged;
      _bw.RunWorkerCompleted += bw_RunWorkerCompleted;
 
      _bw.RunWorkerAsync ("Привет рабочему потоку");
 
      Console.WriteLine ("Нажмите Enter в течение следующих 5 "
                       + "секунд для отмены операции рабочего потока");
      Console.ReadLine();
      if (_bw.IsBusy) _bw.CancelAsync();
      Console.ReadLine();
   }
 
   static void bw_DoWork (object sender, DoWorkEventArgs e)
   {
      for (int i = 0; i < 101; i += 20)
      {
         if (_bw.CancellationPending) { e.Cancel = true; return; }
         _bw.ReportProgress (i);
         Thread.Sleep (1000);    // Это только для демонстрации... в реальных потоках
      }                          // пула никогда так не делайте!
 
      e.Result = 123;            // Это будет передано в RunWorkerCompleted
   }
 
   static void bw_RunWorkerCompleted (object sender,
                                      RunWorkerCompletedEventArgs e)
   {
      if (e.Cancelled)
         Console.WriteLine ("Вы отменили операцию рабочего потока!");
      else if (e.Error != null)
         Console.WriteLine ("Исключение рабочего потока (exception): " + e.Error.ToString());
      else
         Console.WriteLine ("Результат завершенной операции: " + e.Result);   // из DoWork
   }
 
   static void bw_ProgressChanged (object sender,
                                   ProgressChangedEventArgs e)
   {
      Console.WriteLine ("Выполнено " + e.ProgressPercentage + "%");
   }
}

Этот код выведет следующее:

Нажмите Enter в течение следующих 5 секунд для отмены операции рабочего потока
Выполнено 0%
Выполнено 20%
Выполнено 40%
Выполнено 60%
Выполнено 80%
Выполнено 100%
Результат завершенной операции: 123
 
Нажмите Enter в течение следующих 5 секунд для отмены операции рабочего потока
Выполнено 0%
Выполнено 20%
Выполнено 40%

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

BackgroundWorker не изолирован и предоставляет виртуальный метод OnDoWork, предлагая другой шаблон для использования. При реализации потенциально долго работающего метода Вы можете написать дополнительную версию, возвращающую подкласс BackgroundWorker, предварительно сконфигурированный, чтобы выполнить свою работу параллельно. Потребителю тогда нужно только обработать события RunWorkerCompleted и ProgressChanged. Для примера предположим, что пишется долго работающий метод с именем GetFinancialTotals:

public class Client
{
   Dictionary < string,int> GetFinancialTotals (int foo, int bar) { ... }
   ...
}

Переработка кода:

public class Client
{
   public FinancialWorker GetFinancialTotalsBackground (int foo, int bar)
   {
      return new FinancialWorker (foo, bar);
   }
}
 
public class FinancialWorker : BackgroundWorker
{
   public Dictionary < string,int> Result;   // Вы можете добавить типизованные поля.
   public readonly int Foo, Bar;
 
   public FinancialWorker()
   {
      WorkerReportsProgress = true;
      WorkerSupportsCancellation = true;
   }
 
   public FinancialWorker (int foo, int bar) : this()
   {
      this.Foo = foo; this.Bar = bar;
   }
 
   protected override void OnDoWork (DoWorkEventArgs e)
   {
      ReportProgress (0, "Тяжелая работа с этим отчетом...");
 
      // Инициализация данных финансового отчета
      // ...
 
      while (!< отчет завершен >)
      {
         if (CancellationPending) { e.Cancel = true; return; }
         // Выполнить другие шаги вычисления ...
         // ...
         ReportProgress (percentCompleteCalc, "Здесь получено...");
      }
      ReportProgress (100, "Завершено!");
      e.Result = Result = < данные завершенного отчета >;
   }
}

Всякий раз при вызове GetFinancialTotalsBackground будет вызван рабочий поток FinancialWorker: это обертка для управления фоновой операцией, сохраняющей работоспособным интерфейс пользователя программы. Он может сообщать о прогрессе вычислений отчета, может отменить вычисления, и дружественен с формами приложений WPF и Windows Forms, и также обрабатывает исключения в рабочем потоке.

[Interrupt и Abort]

Все блокирующие методы (такие как Sleep, Join, EndInvoke и Wait) блокируют выполнение потока навсегда, если условие разблокировки никогда не удовлетворяется, и не задан таймаут для блокировки. Иногда может быть полезно преждевременно освободить заблокированный поток; например, когда нужно завершить приложение. Это реализуют 2 метода:

• Thread.Interrupt
• Thread.Abort

Метод Abort также может завершить не заблокированный поток — который возможно завис в бесконечном цикле. Abort иногда бывает полезен в некоторых сценариях; Interrupt чаще всего никогда не бывает нужен.

Interrupt и Abort могут доставить значительные неприятности, потому что они предоставляют очевидный способ обойти проблему (баг) кода, который следовало бы исследовать и исправить.

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

static void Main()
{
   Thread t = new Thread (delegate()
   {
      try { Thread.Sleep (Timeout.Infinite); }
      catch (ThreadInterruptedException) { Console.Write ("Принудительное "); }
      Console.WriteLine ("пробуждение!");
   });
   t.Start();
   t.Interrupt();
}

Вывод этого примера:

Принудительное пробуждение!

«Прерывание» (Interrupt) не приводит к завершению потока за исключением случаев, когда исключение ThreadInterruptedException не обрабатывается.

Если Interrupt был вызван на потоке, который не был заблокирован, то поток продолжит выполнение до следующего места блокировки, и при достижении этого места будет выброшено исключение ThreadInterruptedException. Это дает возможность избежать необходимости следующей проверки (что не является потокобезопасным, потому что есть возможность вытеснения между оператором if и worker.Interrupt):

if ((worker.ThreadState & ThreadState.WaitSleepJoin) > 0)
   worker.Interrupt();

Однако «прерывание» потока для общего случая опасно, потому что любые методы фреймворка или сторонних библиотек на стеке вызовов могут неожиданно принять прерывание вместо Вашего назначенного кода. Все что потребовалось бы — краткая блокировка (lock) потока на простом ресурсе синхронизации. Если метод не разработан, чтобы быть «прерванным» (с соответствующим кодом очистки в блоках finally), объекты были бы оставлены в недопустимом состоянии, или ресурсы не полностью были бы освобождены.

Кроме того, применять Interrupt не нужно: если Вы пишете код, который блокируется, то можете достичь того же самого результата безопаснее с помощью конструкции сигнализации — или с помощью cancellation tokens Framework 4.0. Если Вы хотите «разблокировать» в каком-нибудь коде, метод Abort скорее всего будет полезнее.

Abort. Блокировку потока также можно принудительно освободить методом Abort. Это дает эффект, подобный вызову Interrupt, за исключением того, что будет выброшено исключение ThreadAbortException вместо ThreadInterruptedException. Кроме того, исключение будет переброшено в конец блока catch (в попытке окончательно завершить поток), если не был вызван метод Thread.ResetAbort в блоке catch. Тем временем у потока есть состояние ThreadState в значении AbortRequested.

Не обработанное ThreadAbortException является только одним из двух типов исключения, не приводящее к завершению приложения (другой тип из этих двух это AppDomainUnloadException).

Большое отличие между Interrupt и Abort в том, что происходит, когда поток не заблокирован. Принимая во внимание, что Interrupt ждет появления следующей блокировки перед тем, чтобы что-то произошло, Abort выбрасывает исключение на потоке сразу в том месте, где поток выполняется (unmanaged code excepted). Проблема здесь в том, что код .NET Framework может быть прерван — код не безопасен от принудительного обрыва (не abort-safe). Например, если abort возник, когда конструируется FileStream, есть возможность, что unmanaged дескриптор файла останется открытым, пока не будет завершен домен приложения. Это исключает возможность использование Abort в большинстве не тривиальных контекстов. В части 4 этой документации [5] более подробно объясняется, почему Abort небезопасен (см. «Принудительное завершение потоков»).

Хотя есть два случая, где Вы можете безопасно использовать Abort. Один такой случай — когда Вы хотите прекратить работу приложения после принудительного завершения потока через Abort. Хороший пример, когда это может понадобится — написание среды тестирования модулей библиотек (unit-testing framework). Другой случай, где Вы можете безопасно вызвать Abort — непосредственно сам обрываемый поток (потому что точно известно место, где будет оборван процесс вычисления в потоке). Abort, когда поток обрывает этим сам себя, выбросит «не проглатываемое» исключение: такое исключение будет повторно выбрасываться после каждого блока catch. ASP.NET делает именно это, когда Вы вызываете Redirect.

LINQPad обрывает потоки, когда Вы отменяете работу вышедшего из-под контроля запроса. После обрыва ликвидируется и заново создается прикладная область запроса, чтобы избежать потенциально поврежденного состояния, которое могло бы возникнуть в противном случае.

[Безопасная отмена операции потока]

Как было замечено в предыдущей секции, вызов Abort на потоке опасен для большинства сценариев. Альтернативой тогда будет реализация кооперативного шаблона, в котором рабочий поток периодически проверяет состояние флага, сообщающего о необходимости прервать операцию (так же, как это реализуется в BackgroundWorker). Для отмены операции инициатор просто устанавливает флаг, и затем ждет, когда рабочий поток примет его установку и оборвет сам себя. Такой способ реализован во вспомогательном классе BackgroundWorker, и Вы легко можете реализовать то же самое самостоятельно.

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

class RulyCanceler
{
   object _cancelLocker = new object();
   bool _cancelRequest;
   public bool IsCancellationRequested
   {
      get { lock (_cancelLocker) return _cancelRequest; }
   }
 
   public void Cancel() { lock (_cancelLocker) _cancelRequest = true; } 
 
   public void ThrowIfCancellationRequested()
   {
      if (IsCancellationRequested) throw new OperationCanceledException();
   }
}

OperationCanceledException это тип Framework, предназначенный для остановки операции. Хотя так же хорошо будет работать любой класс исключения.

Мы можем использовать это следующим образом:

class Test
{
   static void Main()
   {
      var canceler = new RulyCanceler();
      new Thread (() => {
                           try { Work (canceler); }
                           catch (OperationCanceledException)
                           {
                              Console.WriteLine ("Отменено!");
                           }
                        }).Start();
      Thread.Sleep (1000);
      canceler.Cancel();     // Безопасно отменит операцию рабочего потока Work.
   }
 
   static void Work (RulyCanceler c)
   {
      while (true)
      {
         c.ThrowIfCancellationRequested();
         // ...
         try      { OtherMethod (c); }
         finally  { /* любая необходимая очистка */ }
      }
   }
 
   static void OtherMethod (RulyCanceler c)
   {
      // Тут выполняются какие-то действия...
      c.ThrowIfCancellationRequested();
   }
}

Мы могли бы упростить наш пример путем удаления класса RulyCanceler и добавления к классу Test статического двоичного поля _cancelRequest. Однако это будет означать, что если несколько потоков сразу вызвали Work, то установка _cancelRequest = true отменит работу всех этих потоков. Таким образом, наш класс RulyCanceler является полезной абстракцией. Единственно, что не очень красиво — когда мы смотрим на сигнатуру метода Work, то его намерения не ясны:

static void Work (RulyCanceler c)

Может быть метод Work сам намеревается вызвать Cancel на объекте RulyCanceler? В этом случае ответ нет, поэтому было бы хорошо, если бы это работало в типах системы. Framework 4.0 для этой цели предоставляет Маркеры отмены (cancellation tokens).

Маркеры отмены. Framework 4.0 предоставляет два типа, которые формализуют шаблон кооперативной отмены операции потока, который м продемонстрированы: CancellationTokenSource и CancellationToken. Эти два типа работают в тандеме:

• CancellationTokenSource определяет метод Cancel.
• CancellationToken определяет свойство IsCancellationRequested и метод ThrowIfCancellationRequested.

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

Чтобы использовать эти типы, сначала инстанцируйте объект CancellationTokenSource:

var cancelSource = new CancellationTokenSource();

Затем передайте его свойство Token в метод, для которого хотите реализовать поддержку отмены:

new Thread (() => Work (cancelSource.Token)).Start();

Work должен быть определен следующим образом:

void Work (CancellationToken cancelToken)
{
   cancelToken.ThrowIfCancellationRequested();
   ...
}

Когда захотите отменить работу тела потока Work, просто вызовите Cancel на cancelSource.

CancellationToken в действительности структура, хотя Вы можете считать её классом. При неявном копировании копии ведут себя идентично и ссылаются на оригинальный CancellationTokenSource.

Структура CancellationToken предоставляет два дополнительных полезных члена. Первый это WaitHandle, который возвращает дескриптор ожидания, сигнализирующий об отменяемом токене. Второй Register, который позволит Вам зарегистрировать делегата для callback, который будет вызван при возникновении отмены.

Cancellation tokens используются в самой среде .NET Framework, особенно в следующих классах:

• ManualResetEventSlim и SemaphoreSlim (см. [3])
• CountdownEvent (см. [3])
• Класс Barrier [6]
• BlockingCollection
• PLINQ и библиотека параллельных вычислений TPL [7] (Task Parallel Library)

Большинство этих классов используют cancellation tokens в своих методах Wait. Например, если Вы запустили ожидание Wait на ManualResetEventSlim, и указали cancellation token, другой поток может отменить (вызвать Cancel) это ожидание. Это намного опрятнее и безопаснее, чем вызов Interrupt на заблокированном потоке.

[Ленивая инициализация]

Общая проблема с потоками — как выполнить ленивую инициализацию общего поля так, чтобы это было потокобезопасно. Такая потребность возникает, когда нужно иметь поле, слишком дорогое для создания:

class Foo
{
   public readonly Expensive Expensive = new Expensive();
   ...
}
class Expensive {  /* предположим, что это затратно для конструирования */  }

Проблема тут в том, что этот код при инстанциации Foo влечет за собой потерю производительности для инстанциации Expensive — независимо от того, будет ли когда-нибудь осуществляться доступ к полю Expensive. Очевидный ответ — конструировать экземпляр Expensive по требованию (это называется ленивой инициализацией):

class Foo
{
   Expensive _expensive;
   public Expensive Expensive // Ленивая инстанциация Expensive
   {
      get
      {
         if (_expensive == null) _expensive = new Expensive();
         return _expensive;
      }
   }
   ...
}

Возникает вопрос: безопасно ли это для потоков? Кроме того, что мы получаем доступ _expensive вне блокировки без барьера памяти, предположим что произошло бы, если два потока обратятся к этому свойству одновременно. У них обоих выполнится положительно условие оператора if, и каждый завершится с отдельным экземпляром Expensive. Поскольку это может привести к трудно обнаруживаемым ошибкам, мы можем сказать, что в общем случае это решение не ориентировано на многопоточность (не thread-safe).

Решение проблемы состоит в блокировке вокруг проверки и создания экземпляра объекта Expensive:

Expensive _expensive;
readonly object _expenseLock = new object();
 
public Expensive Expensive
{
   get
   {
      lock (_expenseLock)
      {
         if (_expensive == null) _expensive = new Expensive();
         return _expensive;
      }
   }
}

Lazy< T>. Framework 4.0 предоставляет новый класс Lazy< T>, чтобы помочь с ленивой инициализацией. Если он инстанцирован с аргументом true, то реализуется с потокобезопасной инициализацией, как только что было показано выше.

Lazy< T> в действительности реализует несколько более эффективную версию этого шаблона, которая называется блокировкой с двойной проверкой (double-checked locking). Double-checked locking выполняет дополнительное volatile-чтение, чтобы избежать расходов ресурсов для получения блокировки, когда объект уже инициализирован.

Для использования Lazy< T> инстанцируйте класс с фактическим значением делегата, который укажет, как инициализировать новое значение, и аргументом true. Тогда доступ к этому значению осуществляется через свойство Value:

Lazy< Expensive> _expensive = new Lazy< Expensive>
   (() => new Expensive(), true);
 
public Expensive Expensive { get { return _expensive.Value; } }

Если Вы передадите false в конструктор Lazy< T>, то он реализует не предназначенную для многопоточной среды ленивую инициализацию, которую мы описывали в начале этой секции — Вы должны быть уверены, что хотите использовать Lazy< T> в контексте одного потока.

LazyInitializer. Это статический класс, работающий наподобие Lazy< T>, кроме:

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

Для использования LazyInitializer, вызовите EnsureInitialized перед доступом к полю с передачей ссылки на поле и фактический делегат:

Expensive _expensive;
public Expensive Expensive
{
   get   // Реализует double-checked locking
   { 
      LazyInitializer.EnsureInitialized (ref _expensive,
                                         () => new Expensive());
      return _expensive;
   }
}

Вы также можете передать в другом аргументе, что нужно устроить гонку для потоков, претендующих на инициализацию. Это выглядит похоже на наш оригинальный не потокобезопасный пример с тем отличием, что всегда выиграет только первый пришедший к финишу поток, и процесс инициализации закончится одним одним экземпляром. Достоинство такой техники в том, что она даже быстрее (на многоядерных процессорах), чем double-checked locking — потому что может быть реализована полностью без блокировок. Такая экстремальная реализация необходима редко, и она кое-чего стоит:

• Она медленнее, когда количество потоков, устраивающих гонку на инициализации, меньше, чем количество имеющихся ядер.
• Потенциально могут быть потрачены ресурсы CPU, когда выполняется избыточная инициализация.
• Логика инициализации должна быть реализована потокобезопасно (в этом случае она будет не потокобезопасной, например, если конструктор Expensive пишет данные в статические поля).
• Если инициализатор инстанцирует объект, который требует уничтожения, то «пропадающий впустую» объект не будет расформирован без дополнительной логики.

Как образец, здесь показано, как реализована техника блокировки с двойной проверкой (double-checked locking):

volatile Expensive _expensive;
public Expensive Expensive
{
   get
   {
      if (_expensive == null)          // Первая проверка (внешняя блокировка)
         lock (_expenseLock)
            if (_expensive == null)    // Вторая проверка (внутренняя блокировка)
               _expensive = new Expensive();
      return _expensive;
   }
}

И здесь показано, как реализован шаблон гонки-за-инициализацию:

volatile Expensive _expensive;
public Expensive Expensive
{
   get
   {
      if (_expensive == null)
      {
         var instance = new Expensive();
         Interlocked.CompareExchange (ref _expensive, instance, null);
      }
      return _expensive;
   }
}

[Локальное хранилище потока]

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

Решение для изоляции предлагает локальное хранилище потока (thread-local storage). Вам может быть трудно придумать какое-либо требование: данные, которые Вы хотите поддерживать изолированными, имеют тенденцию быть преходящими по своей природе. Их основное применение — хранить данные «вне диапазона» — чтобы поддерживать инфраструктуру путей выполнения (к этому относится обмен сообщениями, транзакции, токены безопасности). Передача таких данных в параметрах метода выглядит чрезвычайно неуклюже и отчуждает все, кроме Ваших собственных методов; сохранение такой информации в обычных статических полях означает совместное использование её всеми потоками.

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

Есть 3 способа реализовать локальное хранилище потока.

Атрибут ThreadStatic. Самый простой путь задействовать thread-local storage это пометить статическое поле атрибутом ThreadStatic:

[ThreadStatic] static int _x;

Тогда каждый поток увидит отдельную копию _x.

К сожалению, [ThreadStatic] не работает с полями экземпляра (просто с ними ничего не делает); и при этом не работает хорошо с инициализаторами полей — они выполняются только один раз в потоке, который запускается при выполнении статического конструктора. Если Вам нужно работать с полями экземпляра или начать работу со значения не по умолчанию, то ThreadLocal< T> будет лучшим выбором.

ThreadLocal< T>. Это нововведение появилось в Framework 4.0. Оно предоставляет локальное хранилище потока и для статических полей, и для полей экземпляра, и позволит Вам задать значения по умолчанию.

Вот так создается ThreadLocal< int> со значением по умолчанию 3 для каждого потока:

static ThreadLocal< int> _x = new ThreadLocal< int> (() => 3);

Затем Вы используете свойство Value поля _x, чтобы получить его значение, локальное для этого потока. Бонус при использовании ThreadLocal — его значения получают ленивое вычисление: заводская функция вычисляется на первом вызове (для каждого потока).

ThreadLocal< T> и поля экземпляра. ThreadLocal< T> также полезно для полей экземпляра и захваченных локальных переменных. Для примера рассмотрим проблему генерации случайных чисел в многопоточном рабочем окружении. Класс Random не потокобезопасный, так что у нас должна быть либо блокировка вокруг использования Random (ограниченное параллельное использование), или нужно генерировать отдельный объект Random для каждого потока. ThreadLocal< T> упрощает последнее:

var localRandom = new ThreadLocal< Random>(() => new Random());
Console.WriteLine (localRandom.Value.Next());

Наша заводская функция для создания создания объекта Random немного проще, хотя не имеющий параметров конструктор Random полагается на системную тактовую частоту для получения seed, т. е. точки отсчета для вычисления случайного числа. Это может быть одинаково для двух объектов Random, создаваемых с интервалом примерно 10 мс между ними. Вот один из способов исправить это:

var localRandom = new ThreadLocal< Random>
   ( () => new Random (Guid.NewGuid().GetHashCode()) );

Этот способ мы используем в части 5 [7] (см. пример параллельной проверки синтаксиса в «PLINQ»).

GetData и SetData. Третий вариант получить локальное хранилище потока — использование двух методов класса Thread: GetData и SetData. Они сохраняют данные в принадлежащих потоку «слотах». Thread.GetData читает из изолированного хранилища данных потока; Thread.SetData записывает в него. Оба метода требуют объект LocalDataStoreSlot для идентификации слота. Один и тот же слот может использоваться между всеми потоками, и он все еще получит разные значения. Пример:

class Test
{
   // Один и тот же объект LocalDataStoreSlot может использоваться всеми потоками.
   LocalDataStoreSlot _secSlot = Thread.GetNamedDataSlot ("securityLevel");
 
   // Это свойство получит отдельное значение для каждого потока.
   int SecurityLevel
   {
      get
      {
         object data = Thread.GetData (_secSlot);
         return data == null ? 0 : (int) data;    // null == не инстанцировано
      }
      set { Thread.SetData (_secSlot, value); }
   }
   ...

В этом примере мы вызываем Thread.GetNamedDataSlot, который создает именованный слот — это позволяет совместно использует этот слот в приложении. Альтернативно Вы можете управлять областью действия слота, если будете использовать не именованный слот, полученный вызовом Thread.AllocateDataSlot:

class Test
{
   LocalDataStoreSlot _secSlot = Thread.AllocateDataSlot();
   ...

Thread.FreeNamedDataSlot освободит именованный слот данных по всем потокам, но только как все ссылки на тот LocalDataStoreSlot были выброшены из области действия и были обработаны сборщиком мусора. Это гарантирует, что данные не пропадут из потоков, пока они сохраняют ссылку на соответствующий LocalDataStoreSlot, пока этот слот необходим.

[Таймеры]

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

new Thread (delegate()  {
                           while (enabled)
                           {
                              DoSomeAction();
                              Thread.Sleep (TimeSpan.FromHours (24));
                           }
                        }).Start();

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

.NET предоставляет четыре таймера. Два из них многопоточные таймеры общего назначения:

• System.Threading.Timer
• System.Timers.Timer

Другие два специально предназначены для однопоточного использования:

• System.Windows.Forms.Timer (таймер Windows Forms)
• System.Windows.Threading.DispatcherTimer (WPF timer)

Многопоточные таймеры мощнее, точнее и более гибкие; однопоточные таймеры безопаснее и более удобны для запуска простых задач, которые обновляют органы управления Windows Forms или элементы WPF.

Многопоточные таймеры. System.Threading.Timer самый простой многопоточный таймер: у него есть только конструктор и 2 метода. В следующем примере таймер вызывает метод Tick, который выводит «tick…» после истечения 5 секунд, и после этого через каждую секунду, пока пользователь не нажмет Enter:

using System;
using System.Threading;
 
class Program
{
   static void Main()
   {
      // Первый интервал = 5000 мс; последующие интервалы = 1000 мс
      Timer tmr = new Timer (Tick, "tick...", 5000, 1000);
      Console.ReadLine();
      tmr.Dispose();       // Это остановит таймер и выполнит очистку.
   }
 
   static void Tick (object data)
   {
      // Это будет запущено на потоке из пула:
      Console.WriteLine (data);     // Выведет "tick..."
   }
}

Вы можете поменять интервал таймера вызовом Change. Если Вы хотите запустить таймер только 1 раз, задайте Timeout.Infinite в последнем аргументе конструктора.

.NET Framework предоставляет другой класс таймера с тем же именем в пространстве имен System.Timers. Это просто обертка над System.Threading.Timer, использующая ту же самую нижележащую систему и предоставляющая дополнительное удобство. Вот какие функции были добавлены:

• Реализация Component, позволяющая использовать класс визуальным редактором Visual Studio
• Свойство Interval вместо Change
• Elapsedevent вместо делегата callback
• Свойство Enabled для запуска и остановки таймера (со значением по умолчанию false)
• Методы Start и Stop в случае если Вы путаетесь со свойством Enabled
• Флаг AutoReset для указания повторяющегося события (значение по умолчанию true)
• Свойство SynchronizingObject с методами Invoke и BeginInvoke для безопасного вызова методов элементов WPF и органов управления Windows Forms

Ниже дан пример:

using System;
using System.Timers;    // Пространство имен Timers вместо Threading
 
class SystemTimer
{
   static void Main()
   {
      Timer tmr = new Timer();      // Не требуется никаких аргументов
      tmr.Interval = 500;
      tmr.Elapsed += tmr_Elapsed;   // Использует событие вместо делегата
      tmr.Start();                  // Запуск таймера
      Console.ReadLine();
      tmr.Stop();                   // Остановка таймера
      Console.ReadLine();
      tmr.Start();                  // Повторный запуск таймера
      Console.ReadLine();
      tmr.Dispose();                // Остановка таймера навсегда
   }
 
   static void tmr_Elapsed (object sender, EventArgs e)
   {
      Console.WriteLine ("Tick");
   }
}

Многопоточные таймеры используют пул потоков, чтобы позволить нескольким потокам обслуживать много таймеров. Это означает, что метод callback или событие Elapsed может каждый раз быть вызвано на другом потоке. Кроме того, запуск Elapsed всегда сработает (приблизительно) во время — независимо от того, завершилось ли предыдущее выполнение Elapsed. Таким образом, callback-и или обработчики события должны быть потокобезопасными.

Windows multimedia timer. Точность многопоточных таймеров зависит от операционной системы и обычно попадает в интервал 10-20 мс. Если Вам нужна более высокая точность, то можете использовать native interop (?..) и вызов Windows multimedia timer. Это дает точность до 1 мс и определено в winmm.dll. Первый вызов timeBeginPeriod производится для информирования операционной системы, что нужна повышенная точность отсчета времени, затем вызывается timeSetEvent, чтобы запустить multimedia timer. Когда Вы завершили работу с таймером, вызовите timeKillEvent для остановки таймера и timeEndPeriod, чтобы информировать операционную систему, что больше не нужна повышенная точность отсчета времени. Полные примеры использования multimedia timer можно найти в Интернете, используйте для этого поиск по ключевым словам dllimport winmm.dll timesetevent.

Однопоточные таймеры. Библиотека .NET Framework предоставляет таймеры, разработанные для устранения проблем потокобезопасности в приложениях WPF и Windows Forms:

• System.Windows.Threading.DispatcherTimer (WPF)
• System.Windows.Forms.Timer (Windows Forms)

Однопоточные таймеры не разработаны, чтобы функционировать вне своего соответствующего окружения. Например, если Вы используете таймер Windows Forms в приложении Windows Service, то событие Timer не будет срабатывать!

Оба класса таймера наподобие System.Timers.Timer в своих членах предоставляют одинаковые методы и свойства (Interval, Tick, Start, и Stop), и используются также одинаково. Однако они отличаются тем, как они работают внутри себя. Вместо использования пула потоков для генерации событий таймера, таймеры WPF и Windows Forms полагаются на механизм обмена сообщениями нижележащей модели пользовательского интерфейса. Это означает, что событие Tick всегда сработает в том же потоке, где изначально был создан таймер — т. е. в обычном приложении это тот же поток, который используется для обслуживания всего интерфейса пользователя (кнопочки, галочки, окна вывода текста и т. п.). У этого есть определенные достоинства:

• Можно забыть про безопасность потоков.
• Новый Tick никогда не сработает, пока предыдущий Tick не завершит свою обработку.
• Вы можете обновлять элементы пользовательского интерфейса (UI) непосредственно из кода обработки события Tick без необходимости вызывать Control.Invoke или Dispatcher.Invoke.

Это звучит слишком хорошо, чтобы быть правдой, пока Вы не поймете, что программа, применяющая эти таймеры, на самом деле не многопоточная — здесь нет параллельного выполнения кода. Один поток обрабатывает все таймеры — как и делает обработку всех событий UI. Из-за этого мы имеем недостаток однопоточных таймеров:

• Если не сделать быструю обработку обработчика события Tick, то интерфейс пользователя станет неотзывчивым.

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

Что касается точности, то однопоточные таймеры аналогичны многопоточным (десятки миллисекунд), хотя однопоточные таймеры обычно менее точны из-за того, что обработка их события может быть задержана обработкой запросов интерфейса пользователя (или других событий таймера).

[Ссылки]

1. Threading in C# PART 3: USING THREADS site:albahari.com.
2. Потоки на C#. Часть 1: введение.
3. Потоки на C#. Часть 2: основы синхронизации.
4. Параллелизм задач (Task Parallelism).
5. Прекращение работы потока (Aborting Threads).
6. Класс Barrier.
7. Потоки на C#. Часть 5: параллельное программирование.

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Автоустановка драйверов для windows 7 без интернета
  • Three windows in a row
  • Изменение приоритета загрузки в windows
  • How to move windows to another drive
  • Нажмите пкм по значку windows