Основы параллельного программирования с использованием Visual Studio 2010

Параллелизм задач

Разбить на страницы
Показывать лекцию целиком

Введение

Параллелизм задач (task parallelism) - это низкоуровневый подход к распараллеливанию задач с помощью PFX. Классы этого уровня определены в пространстве имен System.Threading.Tasks (Табл. 7.1).

Основные классы пространства имен System.Threading.Task
Класс Описание
Task Представляет асинхронную операцию.
Task <TResult> Представляет асинхронную операцию, которая возвращает значение.
TaskScheduler Объект, обрабатывающий низкоуровневую постановку задач в очередь на потоки.
TaskFactory Для создания объекта Task.
TaskFactory <TResult> Для создания объекта Task, с возвращаемым значением.

Создание и запуск задачи

Как уже писалось в лекции №6 задачи можно создавать различными способами. Один из таких способов - это использование лямда-выражения и метода Task.Factory.StartNew():

Task.Factory.StartNew (() => Console.WriteLine ("Hello World!"));
(рис 7.1) Результат выполнения программы с использованием класса Task

Аналогично, можно было вывести на экран сообщение "Hello World!", вначале создав объект Task, а затем вызвав метод Start():

Task task = new Task (() => Console.Write ("Hello World!"));
task.Start();

Что бы выполнить задачу синхронно (в том же потоке) небходимо вызвать метод RunSynchronously() вместо вызова метода Start():

Task task = new Task (() => Console.Write ("Hello World!"));
task.RunSynchronously()

Класс Task <TResult>, позволяет получить определенный набор данных после завершения выполнения задачи:

Task<int> task = Task.Factory.StartNew< int > (() 
{
int i,c,j;
c=5;
j=5;
i=c+j;
return i;
});
 
   DoSomething();         // Выполнение другой операции асинхронно
   Console.WriteLine("Результат выполнения задачи:"+task.Result);
   Console.ReadLine();
(рис 7.2) Результат выполнения программы с использованием класса Task <TResult>

Создание вложенных задач

Вложенные задача - это экземпляр задачи Task, создаваемый в пользовательском делегате другой задачи. Дочерняя задача - это вложенная задача, создаваемая с помощью параметра AttachedToParent. Задача может создать любое количество дочерних и вложенных задач, ограничиваясь только системными ресурсами. Пример кода, демонстрирующий создание вложенных задач, представлен ниже:

Task parent = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Родительская задача запущена");

                Task.Factory.StartNew(() =>        // Дочерняя задача
                {
                    Console.WriteLine("Дочерняя задача запущена");
                    Thread.SpinWait(5000000);
                    Console.WriteLine("Дочерняя задача выполнена");
                    
                });
            });
            parent.Wait();
            Console.WriteLine("Родительская задача выполнена");
(рис 7.3) Результат выполнения программы с использованием дочерних задач

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

Task parent = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Родительская задача запущена");

                Task.Factory.StartNew(() =>        // Дочерняя задача
                {
                    Console.WriteLine("Дочерняя задача запущена");
                    Thread.SpinWait(5000000);
                    Console.WriteLine("Дочерняя задача выполнена");
                    
                }, TaskCreationOptions.AttachedToParent);
            });
            parent.Wait();
            Console.WriteLine("Родительская задача выполнена");
(рис 7.4) Результат выполнения программы с использованием дочерних задач и параметра AttachedToParent

Ожидание задач

Возможно, осуществлять ожидание выполнения задач двумя различными способами:

  • С помощью метода Wait(). Один из возможных форматов объявления этого метода представлен ниже:
    public Wait(TimeSpan timeout)

    где timeout - время ожидания в миллисекундах.

  • C помощью обращения к свойству Result (свойство класса Task<TResult>):
    public TResult Result;
  • Можно также одновременно ожидать завершения нескольких задач с помощью метода Task.WaitAll() который ожидает завершения всех указанных задач и метода Task.WaitAny() который ожидает завершения какой-либо задачи. Форматы объявления этих методов представлены ниже:

    Public static void WaitAll(Task[]);
    Public static void WaitAny(Task[]);

    где Task[] - массив экземпляров класса Task.

    Метод WaitAll() ожидает выполнение всех остальных задач, по завершению работы задач вызывает исключение типа AggregateException, которое содержит все исключения неудачно завершившихся задач.

    Вызов метода WaitAny() эквивалентен ожиданию ManualResetEventSlim, который переводится в сигнальное состояние при завершении конкретной задачи.

    Обработка ошибок в задачах

    При ожидании завершения задачи с помощью метода Wait(), или свойства Result, любое необработанное исключение будет передано вызывающему коду, а именно объекту AggregationException, что делает не обязательным обработку исключений внутри самой задачи:

    int x = 0;
    Task<int> z = Task.Factory.StartNew (() => 10 / x);
    try
    {
      Console.WriteLine (calc.Result);
    }
    catch (AggregateException aex)
    {
      Console.Write (aex.InnerException.Message);  
    }

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

    (рис 7.5) Результат выполнения программы с обработкой исключения

    Отмена выполнения задач

    Отмена выполнения заданий осуществляется с помощью специальных маркеров отмены (token). Структура CancellationToken - распространяет уведомление о том, что операции следует отменить. При запуске задачи можно передать маркер отмены, что позволит отменить выполнение задачи:

    CancellationTokenSource cancelSource = new CancellationTokenSource();
    CancellationToken token = cancelSource.Token;
     
    Task task = Task.Factory.StartNew (() => 
    {
       token.ThrowIfCancellationRequested();  // Проверяем запрос отмены
     }, token);
    ...
    cancelSource.Cancel();

    Для отмененной задачи вызывается исключение - AggregateException, которое необходимо обработать и проверить следующим образом:

    try 
    {
      task.Wait();
    }
    catch (AggregateException ex)
    {
      if (ex.InnerException is OperationCanceledException)
        Console.Write ("Задание отменено");
    }
    (рис 7.6) Результат выполнения программы с использованием отмены задачи

    Продолжения

    Для того что бы упорядочить выполнение задач в программе, т.е. к примеру что бы Задача 3 выполнялась сразу же после выполнения Задачи 2, а Задача 2 после завершения Задачи 1, необходимо использовать метод ContinueWith() класса - Task. Форма объявление данного метода представлена ниже:

    public Task ContinueWith(Action<Task>)

    Пример работы метода ContinueWith() представлен ниже:

    Task task1 = Task.Factory.StartNew (() => Console.Write ("Hello "));
    Task task2 = task1.ContinueWith (main => Console.Write ("World!"));

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

    (рис 7.7) Результат выполнения программы с использование метода ContinueWith()

    Продолжения и Task <TResult>

    В классе Task<TResult>, также можно использовать продолжение и возвращать некоторые данные, с использованием последовательности вычисления:

    Task.Factory.StartNew<int>(() => 10)
         .ContinueWith(main => main.Result * 2)
         .ContinueWith(main => Math.Sqrt(main.Result))
         .ContinueWith(main => Console.WriteLine(main.Result));
    (рис 7.8) Результат выполнения программы с использование продолжения в классе Task<TResult>

    Продолжения и исключения

    Продолжение может определить, было ли сгенерировано исключение родительской задачей, с помощью свойства Exception родительской задачи. Следующий фрагмент кода будет выводить подробную информацию об исключении NullReferenceException:

    Task task1 = Task.Factory.StartNew (() => { throw null; });
    Task task2 = task1.ContinueWith (main => Console.Write (main.Exception));
    (рис 7.9) Результат выполнения программы с использование продолжения и обработки исключения NullReferenceException

    Продолжения и дочерние задачи

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

    TaskCreationOptions main = TaskCreationOptions.AttachedToParent;
    Task.Factory.StartNew (() =>
    {
      Task.Factory.StartNew (() => { throw null; }, main);
      Task.Factory.StartNew (() => { throw null; }, main);
      Task.Factory.StartNew (() => { throw null; }, main);
    })
    .ContinueWith (p => Console.WriteLine (p.Exception),
                        TaskContinuationOptions.OnlyOnFaulted);
    (рис 7.10) Результат выполнения программы с использование продолжения и дочерних задач

    Продолжения предыдущих задач

    Метод ContinueWhenAll() используется для того, что бы выполнить продолжение конкретной задачи, после выполнения нескольких предыдущих задач:

    Task<int> task1 = Task.Factory.StartNew (() => 200);
    Task<int> task2 = Task.Factory.StartNew (() => 200);
     
    Task<int> task3 = Task<int>.Factory.ContinueWhenAll (
      new[] { task1, task2 }, tasks => tasks.Sum (t => t.Result));
     Console.WriteLine (task3.Result);
    (рис 7.11) Результат выполнения программы с использование метода Метод ContinueWhenAll()

    Несколько продолжений одной задачи

    Метод ContinueWith() позволяет создавать несколько продолжений одной задачи. Когда предыдущая задача завершается, все продолжения запускаются одновременно. Пример использования метода ContinueWith() представлен ниже:

    var t = Task.Factory.StartNew (() => Thread.Sleep (1000));
    t.ContinueWith (main => Console.Write ("X"));
    t.ContinueWith (main => Console.Write ("Y"));
    (рис 7.12) Результат выполнения программы с использование метода ContinueWith()

    Планировщики заданий и пользовательский интерфейс

    Планировщик задач (task scheduler) назначает задания определенным потокам. Все задания связаны с определенным планировщиком, который представлен абстрактным классом TaskScheduler. .Net Framework предоставляет две конкретные реализации:

  • Планировщик по умолчанию (default scheduler), который работает совместно с пулом потоков CLR;
  • Планировщик контекста синхронизации (synchronization context scheduler), который предназначен для упрощения работы с WPF и Windows Forms, которые требуют, чтобы обращение к элементам пользовательского интерфейса и элементам управления происходило только из потока, в котором они были созданы.
  • Что бы в фоновом режиме получить данные от Web-сервиса и затем, на основе полученных результатов, обновить метку (label) с именем lblResult. Разбиваем эту задачу на две подзадачи:

  • Вызвать метод для получения данных от Web-сервиса (родительская задача).
  • Обновить lblResult на основе полученных результатов (задача-продолжение).
  • Если указать для задачи-продолжения планировщик контекста синхронизации, полученный при создании окна, можно обновить lblResult:

    public partial class MyWindow : Window
    {
      TaskScheduler _uiScheduler;   // Объявляем TaskScheduler
      public MyWindow()
      {    
        InitializeComponent();
     
        _uiScheduler = TaskScheduler.
    FromCurrentSynchronizationContext();
     
        Task.Factory.StartNew<string> (SomeComplexWebService)
          .ContinueWith (main => lblResult.Content = main.Result, _uiScheduler);
      }
     
      string SomeComplexWebService() { ... }
    }
    Страницы:

    Введение

    Параллелизм задач (task parallelism) - это низкоуровневый подход к распараллеливанию задач с помощью PFX. Классы этого уровня определены в пространстве имен System.Threading.Tasks (Табл. 7.1).

    Основные классы пространства имен System.Threading.Task
    Класс Описание
    Task Представляет асинхронную операцию.
    Task <TResult> Представляет асинхронную операцию, которая возвращает значение.
    TaskScheduler Объект, обрабатывающий низкоуровневую постановку задач в очередь на потоки.
    TaskFactory Для создания объекта Task.
    TaskFactory <TResult> Для создания объекта Task, с возвращаемым значением.

    Создание и запуск задачи

    Как уже писалось в лекции №6 задачи можно создавать различными способами. Один из таких способов - это использование лямда-выражения и метода Task.Factory.StartNew():

    Task.Factory.StartNew (() => Console.WriteLine ("Hello World!"));
    (рис 7.1) Результат выполнения программы с использованием класса Task

    Аналогично, можно было вывести на экран сообщение "Hello World!", вначале создав объект Task, а затем вызвав метод Start():

    Task task = new Task (() => Console.Write ("Hello World!"));
    task.Start();

    Что бы выполнить задачу синхронно (в том же потоке) небходимо вызвать метод RunSynchronously() вместо вызова метода Start():

    Task task = new Task (() => Console.Write ("Hello World!"));
    task.RunSynchronously()

    Класс Task <TResult>, позволяет получить определенный набор данных после завершения выполнения задачи:

    Task<int> task = Task.Factory.StartNew< int > (() 
    {
    int i,c,j;
    c=5;
    j=5;
    i=c+j;
    return i;
    });
     
       DoSomething();         // Выполнение другой операции асинхронно
       Console.WriteLine("Результат выполнения задачи:"+task.Result);
       Console.ReadLine();
    (рис 7.2) Результат выполнения программы с использованием класса Task <TResult>

    Создание вложенных задач

    Вложенные задача - это экземпляр задачи Task, создаваемый в пользовательском делегате другой задачи. Дочерняя задача - это вложенная задача, создаваемая с помощью параметра AttachedToParent. Задача может создать любое количество дочерних и вложенных задач, ограничиваясь только системными ресурсами. Пример кода, демонстрирующий создание вложенных задач, представлен ниже:

    Task parent = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("Родительская задача запущена");
    
                    Task.Factory.StartNew(() =>        // Дочерняя задача
                    {
                        Console.WriteLine("Дочерняя задача запущена");
                        Thread.SpinWait(5000000);
                        Console.WriteLine("Дочерняя задача выполнена");
                        
                    });
                });
                parent.Wait();
                Console.WriteLine("Родительская задача выполнена");
    (рис 7.3) Результат выполнения программы с использованием дочерних задач

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

    Task parent = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("Родительская задача запущена");
    
                    Task.Factory.StartNew(() =>        // Дочерняя задача
                    {
                        Console.WriteLine("Дочерняя задача запущена");
                        Thread.SpinWait(5000000);
                        Console.WriteLine("Дочерняя задача выполнена");
                        
                    }, TaskCreationOptions.AttachedToParent);
                });
                parent.Wait();
                Console.WriteLine("Родительская задача выполнена");
    (рис 7.4) Результат выполнения программы с использованием дочерних задач и параметра AttachedToParent

    Ожидание задач

    Возможно, осуществлять ожидание выполнения задач двумя различными способами:

  • С помощью метода Wait(). Один из возможных форматов объявления этого метода представлен ниже:
    public Wait(TimeSpan timeout)

    где timeout - время ожидания в миллисекундах.

  • C помощью обращения к свойству Result (свойство класса Task<TResult>):
    public TResult Result;
  • Можно также одновременно ожидать завершения нескольких задач с помощью метода Task.WaitAll() который ожидает завершения всех указанных задач и метода Task.WaitAny() который ожидает завершения какой-либо задачи. Форматы объявления этих методов представлены ниже:

    Public static void WaitAll(Task[]);
    Public static void WaitAny(Task[]);

    где Task[] - массив экземпляров класса Task.

    Метод WaitAll() ожидает выполнение всех остальных задач, по завершению работы задач вызывает исключение типа AggregateException, которое содержит все исключения неудачно завершившихся задач.

    Вызов метода WaitAny() эквивалентен ожиданию ManualResetEventSlim, который переводится в сигнальное состояние при завершении конкретной задачи.

    Обработка ошибок в задачах

    При ожидании завершения задачи с помощью метода Wait(), или свойства Result, любое необработанное исключение будет передано вызывающему коду, а именно объекту AggregationException, что делает не обязательным обработку исключений внутри самой задачи:

    int x = 0;
    Task<int> z = Task.Factory.StartNew (() => 10 / x);
    try
    {
      Console.WriteLine (calc.Result);
    }
    catch (AggregateException aex)
    {
      Console.Write (aex.InnerException.Message);  
    }

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

    (рис 7.5) Результат выполнения программы с обработкой исключения

    Отмена выполнения задач

    Отмена выполнения заданий осуществляется с помощью специальных маркеров отмены (token). Структура CancellationToken - распространяет уведомление о том, что операции следует отменить. При запуске задачи можно передать маркер отмены, что позволит отменить выполнение задачи:

    CancellationTokenSource cancelSource = new CancellationTokenSource();
    CancellationToken token = cancelSource.Token;
     
    Task task = Task.Factory.StartNew (() => 
    {
       token.ThrowIfCancellationRequested();  // Проверяем запрос отмены
     }, token);
    ...
    cancelSource.Cancel();

    Для отмененной задачи вызывается исключение - AggregateException, которое необходимо обработать и проверить следующим образом:

    try 
    {
      task.Wait();
    }
    catch (AggregateException ex)
    {
      if (ex.InnerException is OperationCanceledException)
        Console.Write ("Задание отменено");
    }
    (рис 7.6) Результат выполнения программы с использованием отмены задачи

    Продолжения

    Для того что бы упорядочить выполнение задач в программе, т.е. к примеру что бы Задача 3 выполнялась сразу же после выполнения Задачи 2, а Задача 2 после завершения Задачи 1, необходимо использовать метод ContinueWith() класса - Task. Форма объявление данного метода представлена ниже:

    public Task ContinueWith(Action<Task>)

    Пример работы метода ContinueWith() представлен ниже:

    Task task1 = Task.Factory.StartNew (() => Console.Write ("Hello "));
    Task task2 = task1.ContinueWith (main => Console.Write ("World!"));

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

    (рис 7.7) Результат выполнения программы с использование метода ContinueWith()

    Продолжения и Task <TResult>

    В классе Task<TResult>, также можно использовать продолжение и возвращать некоторые данные, с использованием последовательности вычисления:

    Task.Factory.StartNew<int>(() => 10)
         .ContinueWith(main => main.Result * 2)
         .ContinueWith(main => Math.Sqrt(main.Result))
         .ContinueWith(main => Console.WriteLine(main.Result));
    (рис 7.8) Результат выполнения программы с использование продолжения в классе Task<TResult>

    Продолжения и исключения

    Продолжение может определить, было ли сгенерировано исключение родительской задачей, с помощью свойства Exception родительской задачи. Следующий фрагмент кода будет выводить подробную информацию об исключении NullReferenceException:

    Task task1 = Task.Factory.StartNew (() => { throw null; });
    Task task2 = task1.ContinueWith (main => Console.Write (main.Exception));
    (рис 7.9) Результат выполнения программы с использование продолжения и обработки исключения NullReferenceException

    Продолжения и дочерние задачи

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

    TaskCreationOptions main = TaskCreationOptions.AttachedToParent;
    Task.Factory.StartNew (() =>
    {
      Task.Factory.StartNew (() => { throw null; }, main);
      Task.Factory.StartNew (() => { throw null; }, main);
      Task.Factory.StartNew (() => { throw null; }, main);
    })
    .ContinueWith (p => Console.WriteLine (p.Exception),
                        TaskContinuationOptions.OnlyOnFaulted);
    (рис 7.10) Результат выполнения программы с использование продолжения и дочерних задач

    Продолжения предыдущих задач

    Метод ContinueWhenAll() используется для того, что бы выполнить продолжение конкретной задачи, после выполнения нескольких предыдущих задач:

    Task<int> task1 = Task.Factory.StartNew (() => 200);
    Task<int> task2 = Task.Factory.StartNew (() => 200);
     
    Task<int> task3 = Task<int>.Factory.ContinueWhenAll (
      new[] { task1, task2 }, tasks => tasks.Sum (t => t.Result));
     Console.WriteLine (task3.Result);
    (рис 7.11) Результат выполнения программы с использование метода Метод ContinueWhenAll()

    Несколько продолжений одной задачи

    Метод ContinueWith() позволяет создавать несколько продолжений одной задачи. Когда предыдущая задача завершается, все продолжения запускаются одновременно. Пример использования метода ContinueWith() представлен ниже:

    var t = Task.Factory.StartNew (() => Thread.Sleep (1000));
    t.ContinueWith (main => Console.Write ("X"));
    t.ContinueWith (main => Console.Write ("Y"));
    (рис 7.12) Результат выполнения программы с использование метода ContinueWith()

    Планировщики заданий и пользовательский интерфейс

    Планировщик задач (task scheduler) назначает задания определенным потокам. Все задания связаны с определенным планировщиком, который представлен абстрактным классом TaskScheduler. .Net Framework предоставляет две конкретные реализации:

  • Планировщик по умолчанию (default scheduler), который работает совместно с пулом потоков CLR;
  • Планировщик контекста синхронизации (synchronization context scheduler), который предназначен для упрощения работы с WPF и Windows Forms, которые требуют, чтобы обращение к элементам пользовательского интерфейса и элементам управления происходило только из потока, в котором они были созданы.
  • Что бы в фоновом режиме получить данные от Web-сервиса и затем, на основе полученных результатов, обновить метку (label) с именем lblResult. Разбиваем эту задачу на две подзадачи:

  • Вызвать метод для получения данных от Web-сервиса (родительская задача).
  • Обновить lblResult на основе полученных результатов (задача-продолжение).
  • Если указать для задачи-продолжения планировщик контекста синхронизации, полученный при создании окна, можно обновить lblResult:

    public partial class MyWindow : Window
    {
      TaskScheduler _uiScheduler;   // Объявляем TaskScheduler
      public MyWindow()
      {    
        InitializeComponent();
     
        _uiScheduler = TaskScheduler.
    FromCurrentSynchronizationContext();
     
        Task.Factory.StartNew<string> (SomeComplexWebService)
          .ContinueWith (main => lblResult.Content = main.Result, _uiScheduler);
      }
     
      string SomeComplexWebService() { ... }
    }
    Вернуться к учебному плану