Опубликован: 15.10.2009 | Доступ: свободный | Студентов: 896 / 248 | Оценка: 4.42 / 4.20 | Длительность: 08:22:00
Специальности: Программист
Лекция 5:

Программирование с использованием Task Parallel Library (TPL)

< Лекция 4 || Лекция 5: 12 || Лекция 6 >
Аннотация: В этой лекции рассматривается один из уровней библиотеки PFX - программирование на уровне задач (TPL). Во второй части лекции показана практическая реализация параллелизма в рекурсивных функциях.

Как было отмечено во введении, в библиотеке PFX поддерживаются три уровня параллелизма:

  • уровень декларативной обработки данных (PLINQ, см. Лекцию 7),
  • уровень императивной обработки данных (конструкции Parallel.For/ForEach/Invoke, см. "Лекции 2" и "4" ), а также
  • уровень императивной работы с задачами (tasks).

Последний уровень является базовым в PFX, имеет специальное название - Task Parallel Library (TPL), и на его основе могут быть реализованы два других уровня. Программирование на уровне задач требует больших усилий со стороны программиста, однако, этот уровень является универсальным, а потому, более гибким - с помощью задач можно построить любое параллельное приложение в отличие от специальных шаблонов Parallel.For/ForEach/Invoke.

На первый взгляд, класс System.Threading.Tasks.Task является аналогом пула потоков в .NET (имеется в виду класс System.Threading.ThreadPool ). Действительно, общность принципов работы с этими классами налицо - например, запросить у пула поток для выполнения программа может следующим образом:

ThreadPool.QueueUserWorkItem(delegate { ... });

Работа с классом Task в этом случае будет выглядеть так:

Task.Create(delegate { ... });

Однако, класс Task и в целом библиотека TPL представляет несравнимо большие возможности, чем стандартный пул потоков. Например, если вы хотите параллельно выполнить три задачи и дождаться завершения их выполнения, то используя пул потоков вы можете написать:

//создадим три события "Выполнение задачи завершено"
using(ManualResetEvent mre1 = new ManualResetEvent(false))
using(ManualResetEvent mre2 = new ManualResetEvent(false))
using(ManualResetEvent mre3 = new ManualResetEvent(false))
      {
//запросим у пула потоков параллельное исполнение 3-х задач
          ThreadPool.QueueUserWorkItem(delegate
          {
              A();
              mre1.Set();
          });
          ThreadPool.QueueUserWorkItem(delegate
          {
              B();
              mre2.Set();
          });
          ThreadPool.QueueUserWorkItem(delegate
          {
              C();
              mre3.Set();
          });
 	//дождемся выполнения всех задач
          WaitHandle.WaitAll(new WaitHandle[]{mre1, mre2, mre3});
      }

Аналогичный код с использованием TPL будет выглядеть следующим образом:

Task t1 = Task.Create(delegate { A(); });
Task t2 = Task.Create(delegate { B(); });
Task t3 = Task.Create(delegate { C(); });
t1.Wait();
t2.Wait();
t3.Wait();

Будем называть код делегата типа Action<Object>, передаваемого при создании задачи, телом задачи.

Код выше можно переписать немного элегантнее:

Task t1 = Task.Create(delegate { A(); });
Task t2 = Task.Create(delegate { B(); });
Task t3 = Task.Create(delegate { C(); });
Task.WaitAll(t1, t2, t3);

При этом нужно понимать, что реально исполнение задачи одним из рабочих потоков начнется не сразу же после вызова метода Task.Create, а через некоторое время. То есть, так же как и в случае с ThreadPool, при своем создании задача просто помещается в очередь планировщика. Решение о моменте запуска задачи на исполнение потоком принимает планировщик в соответствии с дисциплиной планирования исполнения задач. Более подробно о дисциплинах и принципах планирования см. "Лекцию 3" .

Внимательный читатель может заметить, что библиотека PFX позволяет еще проще реализовать параллельный запуск задач с помощью метода Parallel.Invoke:

Parallel.Invoke( ()=>A() , ()=>B() , ()=>C() );

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

... // подготовительная работа для задачи А
Task t1 = Task.Create(delegate { A(); });
... // подготовительная работа для задачи B
Task t2 = Task.Create(delegate { B(); });
... // подготовительная работа для задачи C
Task t3 = Task.Create(delegate { C(); });
Task.WaitAll(t1, t2, t3);

Выше мы рассмотрели примеры порождения новых задач и ожидания их завершения. Существуют и другие возможности управления исполнением задач. Так, например, для того чтобы отменить выполнение задачи можно использовать метод Task.Cancel. При вызове данного метода возможны два варианта:

  1. Если задача не была отправлена планировщиком на исполнение каким-либо рабочим потоком, то произойдет изъятие задачи из очереди планировщка
  2. Если задача уже исполняется каким-либо рабочим потоком, то из-за соображений надежности ее исполнение не будет прервано, однако свойство Task.IsCanceled примет значение True, что позволит коду задачи, проанализировав значение этого свойства, завершить свое исполнение.

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

Для того чтобы из тела задачи получить доступ к объекту Task можно воспользоваться статическим свойством Task.Current:

static void Main(string[] args)
{
      Task a = Task.Current;
      //a==null
      Task.Create(delegate { A(); });
}

//выведет "False"
static void A() { Console.WriteLine(Task.Current.IsCompleted); }

Рассмотрим свойства задачи Task.Parent и Task.Creator. Эти свойства отвечают за иерархические связи на множестве задач. Свойство Task.Creator указывает на задачу в теле которой была создана данная задача, другими словами Task.Creator равно Task.Current материнской задачи. Свойство Task.Parent совпадает со свойством Task.Creator, за исключением того случая, когда при создании задачи была указана опция TaskCreationOptions.Detached (в последнем случае, у задачи нет "родителя"). Отметим, что задача завершает свое исполнение тогда и только тогда, когда все ее дочерние задачи также завершают свое исполнение:

Task p = Task.Create(delegate 
{ 
    Task c1 = Task.Create(...); 
    Task c2 = Task.Create(...); 
    Task c3 = Task.Create(...); 
}); 
... 
p.Wait(); // ожидание завершения задач p, c1, c2 и c3

В текущей версии библиотеки PFX work-stealing планировщик представлен классом System.Threading.Tasks.TaskManager. При создании объекта TaskManager программст может указать ряд значений параметров, которые будут влиять на планирование исполнения задач, переданных данному планировщику. Программист может создавать несколько планировщиков, а при создании задачи указывать какой планировщик будет контролировать исполнение задачи.

Параметры планировщика описываются классом TaskManagerPolicy. Перечислим эти параметры:

  1. minProcessors - минимальное число процессоров, используемое планировщиком. По умолчанию - 1 процессор;
  2. idealProcessors - оптимальное число процессоров, используемое планировщиком. По умолчанию - количество доступных процессоров в системе;
  3. idealThreadsPerProcessor - оптимальное число потоков, создаваемых планировщиком для каждого процессора. По умолчанию - один поток на один процессор;
  4. maxStackSize - максимальный объем стека для рабочих потоков;
  5. threadPriority - приоритет рабочих потоков. По умолчанию - ThreadPriority.Normal ;

Задачи являются одним из базовых элементов библиотеки PFX, другим базовым элементом являются Future, работа с которыми будет описана в "лекции 6" .

Замечание:

Отметим, что начиная с версии .NET 4 CTP TPL входит в состав сборки mscorlib.dll, поэтому теперь любое приложение сможет иметь доступ к TPL без необходимости включения в свой состав дополнительных сборок.

< Лекция 4 || Лекция 5: 12 || Лекция 6 >
Максим Полищук
Максим Полищук
"...Изучение и анализ примеров.
В и приведены описания и приложены исходные коды параллельных программ..."
Непонятно что такое - "В и приведены описания" и где именно приведены и приложены исходные коды.