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


Параллелизм задач (библиотека параллельных задач)

Библиотека параллельных задач (TPL), как и предполагает ее имя, основывается на концепции задачи. Термин параллелизм задач означает одновременное выполнение одной или нескольких разных задач. Задача представляет собой асинхронную операцию и в некотором роде напоминает создание нового потока или рабочего элемента ThreadPool, но на более высоком уровне абстракции. Задачи предоставляют два основных преимущества.

  • Более эффективное и масштабируемое использование системных ресурсов.

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

  • Больший программный контроль по сравнению с потоком или рабочим элементом.

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

По этим причинам в .NET Framework 4 задачи — предпочтительные API для написания многопоточного, асинхронного и параллельного кода.

Неявное создание и запуск задач

Метод Parallel.Invoke предоставляет удобный способ одновременного запуска любого числа произвольных операторов. Достаточно передать в делегат Action для каждого рабочего элемента. Самым простым способом создания этих делегатов является использование лямбда-выражений. Лямбда-выражение может вызвать именованный метод или предоставить встроенный код. В следующем примере показан вызов базового метода Invoke, который создает и запускается две задачи, выполняемые параллельно.

ПримечаниеПримечание

В этой документации для определения делегатов в библиотеке параллельных задач используются лямбда-выражения.Сведения о лямбда-выражениях в C# или Visual Basic см. в разделе Лямбда-выражения в PLINQ и библиотеке параллельных задач.

Parallel.Invoke(Sub() DoSomeWork(), Sub() DoSomeOtherWork())
Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
ПримечаниеПримечание

Число экземпляров Task, созданных Invoke в фоновом режиме, не обязательно равно числу предоставленных делегатов.Библиотека параллельных задач может применять различные оптимизации, особенно с большим количеством делегатов.

Дополнительные сведения см. в разделе Практическое руководство. Использование метода Parallel.Invoke для выполнения параллельных операций.

Для большего контроля над выполнением задач или возврата значения из задачи необходимо более явно работать с объектами Task.

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

Задача представляется классом System.Threading.Tasks.Task. Задача, возвращающая значение, представляется классом System.Threading.Tasks.Task<TResult>, унаследованным от Task. Объект задачи обрабатывает сведения инфраструктуры и предоставляет методы и свойства, которые доступны из вызывающего потока на протяжении времени существования задачи. Например, можно получить доступ к свойству Status задачи в любое время для определения того, было ли начато ее выполнение, завершилась ли она, была ли отменена или создала исключение. Состояние представлено перечислением TaskStatus.

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

        ' Create a task and supply a user delegate by using a lambda expression.
        Dim taskA = New Task(Sub() Console.WriteLine("Hello from taskA."))

        ' Start the task.
        taskA.Start()

        ' Output a message from the joining thread.
        Console.WriteLine("Hello from the joining thread.")

        ' Output:
        ' Hello from the joining thread.
        ' Hello from taskA. 

            // Create a task and supply a user delegate by using a lambda expression.
            var taskA = new Task(() => Console.WriteLine("Hello from taskA."));

            // Start the task.
            taskA.Start();

            // Output a message from the joining thread.
            Console.WriteLine("Hello from the calling thread.");


            /* Output:
             * Hello from the joining thread.
             * Hello from taskA. 
             */

Для создания и запуска задачи в одной операции можно также использовать метод StartNew. Это предпочтительный способ создания и запуска задач, если нет необходимости разделять создание и планирование; см. пример ниже.

' Better: Create and start the task in one operation.
Dim taskA = Task.Factory.StartNew(Sub() Console.WriteLine("Hello from taskA."))

' Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.")
// Create and start the task in one operation.
var taskA = Task.Factory.StartNew(() => Console.WriteLine("Hello from taskA."));

// Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.");

Задача предоставляет статическое свойство Factory, возвращающее экземпляр по умолчанию TaskFactory, чтобы можно было вызвать метод как Task.Factory.StartNew(…). Кроме того, поскольку задачи принадлежат к типу System.Threading.Tasks.Task<TResult>, в этом примере каждая из них имеет открытое свойство Result, содержащее результат вычислений. Задачи выполняются асинхронно и могут завершиться в любом порядке. Если обратиться к свойству Result до завершения вычисления, оно заблокирует поток до тех пор, пока значение не станет доступно.

Dim taskArray() = {Task(Of Double).Factory.StartNew(Function() DoComputation1()),
                   Task(Of Double).Factory.StartNew(Function() DoComputation2()),
                   Task(Of Double).Factory.StartNew(Function() DoComputation3())}


Dim results() As Double
ReDim results(taskArray.Length)
For i As Integer = 0 To taskArray.Length
    results(i) = taskArray(i).Result
Next
Task<double>[] taskArray = new Task<double>[]
   {
       Task<double>.Factory.StartNew(() => DoComputation1()),

       // May be written more conveniently like this:
       Task.Factory.StartNew(() => DoComputation2()),
       Task.Factory.StartNew(() => DoComputation3())                
   };

double[] results = new double[taskArray.Length];
for (int i = 0; i < taskArray.Length; i++)
    results[i] = taskArray[i].Result;

Дополнительные сведения см. в разделе Практическое руководство. Возвращение значения из задачи.

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


    Class MyCustomData

        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Sub TaskDemo2()
        ' Create the task object by using an Action(Of Object) to pass in custom data
        ' in the Task constructor. This is useful when you need to capture outer variables
        ' from within a loop. 
        ' As an experiement, try modifying this code to capture i directly in the lamda,
        ' and compare results.
        Dim taskArray() As Task
        ReDim taskArray(10)
        For i As Integer = 0 To taskArray.Length - 1
            taskArray(i) = New Task(Sub(obj As Object)
                                        Dim mydata = CType(obj, MyCustomData)
                                        mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId
                                        Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.",
                                                          mydata.Name, mydata.CreationTime, mydata.ThreadNum)
                                    End Sub,
            New MyCustomData With {.Name = i, .CreationTime = DateTime.Now.Ticks}
            )
            taskArray(i).Start()
        Next

    End Sub


       class MyCustomData
       {
        public long CreationTime;
        public int Name;
        public int ThreadNum;
        }

    void TaskDemo2()
    {
        // Create the task object by using an Action(Of Object) to pass in custom data
        // in the Task constructor. This is useful when you need to capture outer variables
        // from within a loop. As an experiement, try modifying this code to 
        // capture i directly in the lambda, and compare results.
        Task[] taskArray = new Task[10];

        for(int i = 0; i < taskArray.Length; i++)
        {
            taskArray[i] = new Task((obj) =>
                {
                                        MyCustomData mydata = (MyCustomData) obj;
                                        mydata.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                        Console.WriteLine("Hello from Task #{0} created at {1} running on thread #{2}.",
                                                          mydata.Name, mydata.CreationTime, mydata.ThreadNum)
                },
            new MyCustomData () {Name = i, CreationTime = DateTime.Now.Ticks}
            );
            taskArray[i].Start();
        }
    }

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

Идентификатор задачи

Каждая задача получает целочисленный идентификатор, уникально определяющий ее в домене приложения. Доступ к нему можно получить с помощью свойства Id. Идентификатор полезен для просмотра сведений о задаче в окнах Параллельные стеки и Параллельные задачи отладчика Visual Studio. Идентификатор создается неактивно, что означает, что он не создается, пока не запрашивается. Следовательно, задача может иметь различные идентификаторы при каждом запуске программы. Дополнительные сведения о просмотре идентификаторов задач в отладчике см. в разделе Использование окна "Параллельные стеки".

Параметры создания задачи

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

Элемент

Описание

None

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

PreferFairness

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

LongRunning

Указывает, что задача представляет длительную операцию.

AttachedToParent

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

Параметры можно объединить с использованием побитовой операции OR. В следующем примере показана задача с параметрами LongRunning и PreferFairness.


Dim task3 = New Task(Sub() MyLongRunningMethod(),
                        TaskCreationOptions.LongRunning Or TaskCreationOptions.PreferFairness)
task3.Start()
var task3 = new Task(() => MyLongRunningMethod(),
                    TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task3.Start();

Создание продолжений задачи

С помощью методов Task.ContinueWith и Task<TResult>.ContinueWith можно указать, чтобы задача запускалась по завершении предшествующей задачи. Делегат задачи продолжения передается в качестве ссылки на предшествующую задачу, чтобы проанализировать ее состояние. Кроме того, определенное пользователем значение можно передать из предшествующей задачи в ее продолжение в свойство Result, чтобы выходные данные предшествующей задачи могли использоваться как входные данные для продолжения. В следующем примере getData запускается программным кодом, затем analyzeData запускается автоматически по завершении getData, а reportData запускается по завершении analyzeData. getData создает байтовый массив, который передается в analyzeData как ее результат. analyzeData обрабатывает этот массив и возвращает результат, тип которого определяется на основании возвращаемого типа метода Analyze. reportData принимает входные данные от analyzeData и выдает результат, тип которого определяется аналогичным образом и который становится доступным в программе в свойстве Result.

        Dim getData As Task(Of Byte()) = New Task(Of Byte())(Function() GetFileData())
        Dim analyzeData As Task(Of Double()) = getData.ContinueWith(Function(x) Analyze(x.Result))
        Dim reportData As Task(Of String) = analyzeData.ContinueWith(Function(y As Task(Of Double)) Summarize(y.Result))

        getData.Start()

        System.IO.File.WriteAllText("C:\reportFolder\report.txt", reportData.Result)

            Task<byte[]> getData = new Task<byte[]>(() => GetFileData());
            Task<double[]> analyzeData = getData.ContinueWith(x => Analyze(x.Result));
            Task<string> reportData = analyzeData.ContinueWith(y => Summarize(y.Result));

            getData.Start();

            //or...
            Task<string> reportData2 = Task.Factory.StartNew(() => GetFileData())
                                        .ContinueWith((x) => Analyze(x.Result))
                                        .ContinueWith((y) => Summarize(y.Result));

            System.IO.File.WriteAllText(@"C:\reportFolder\report.txt", reportData.Result);



С помощью методов ContinueWhenAll и ContinueWhenAny можно продолжить выполнение с нескольких задач. Дополнительные сведения см. в разделах Задачи продолжения и Практическое руководство. Сцепление нескольких задач с помощью продолжений.

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

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

Dim outer = Task.Factory.StartNew(Sub()
                                      Console.WriteLine("Outer task beginning.")
                                      Dim child = Task.Factory.StartNew(Sub()
                                                                            Thread.SpinWait(5000000)
                                                                            Console.WriteLine("Detached task completed.")
                                                                        End Sub)
                                  End Sub)
outer.Wait()
Console.WriteLine("Outer task completed.")

' Output:
'     Outer task beginning.
'     Outer task completed.
'    Detached child completed.
            var outer = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Outer task beginning.");

                var child = Task.Factory.StartNew(() =>
                {
                    Thread.SpinWait(5000000);
                    Console.WriteLine("Detached task completed.");
                });

            });

            outer.Wait();
            Console.WriteLine("Outer task completed.");

            /* Output:
                Outer task beginning.
                Outer task completed.
                Detached task completed.

             */

Обратите внимание, что внешняя задача не ожидает завершения вложенной задачи.

Создание дочерних задач

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

Dim parent = Task.Factory.StartNew(Sub()
                                       Console.WriteLine("Parent task beginning.")
                                       Dim child = Task.Factory.StartNew(Sub()
                                                                             Thread.SpinWait(5000000)
                                                                             Console.WriteLine("Attached child completed.")
                                                                         End Sub,
                                                                         TaskCreationOptions.AttachedToParent)

                                   End Sub)
outer.Wait()
Console.WriteLine("Parent task completed.")

' Output:
'     Parent task beginning.
'     Attached child completed.
'     Parent task completed.
var parent = Task.Factory.StartNew(() =>
{
    Console.WriteLine("Parent task beginning.");

    var child = Task.Factory.StartNew(() =>
    {
        Thread.SpinWait(5000000);
        Console.WriteLine("Attached child completed.");
    }, TaskCreationOptions.AttachedToParent);

});

parent.Wait();
Console.WriteLine("Parent task completed.");

/* Output:
    Parent task beginning.
    Attached task completed.
    Parent task completed.
 */

Дополнительные сведения см. в разделе Вложенные и дочерние задачи.

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

Типы System.Threading.Tasks.Task и System.Threading.Tasks.Task<TResult> предоставляют несколько перегрузок методов Task.Wait и Task<TResult>.Wait, которые позволяют ожидать завершения задачи. Кроме того, перегрузки статического метода Task.WaitAll и метода Task.WaitAny позволяют ожидать завершения какого-либо или всех массивов задач.

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

  • Основной поток зависит от конечного результата, вычисленного задачей.

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

В следующем примере показан базовый шаблон, в котором не указана обработка исключений.

Dim tasks() =
{
    Task.Factory.StartNew(Sub() MethodA()),
    Task.Factory.StartNew(Sub() MethodB()),
    Task.Factory.StartNew(Sub() MethodC())
}

' Block until all tasks complete.
Task.WaitAll(tasks)

' Continue on this thread...
Task[] tasks = new Task[3]
{
    Task.Factory.StartNew(() => MethodA()),
    Task.Factory.StartNew(() => MethodB()),
    Task.Factory.StartNew(() => MethodC())
};

//Block until all tasks complete.
Task.WaitAll(tasks);

// Continue on this thread...

Пример обработки исключений см. в разделе Практическое руководство. Обработка исключений, создаваемых задачами.

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

При ожидании задачи неявно ожидаются все ее дочерние задачи, созданные с помощью параметра AttachedToParent TaskCreationOptions. Task.Wait выполняет возврат немедленно, если задача уже завершена. Любые исключения, вызванные задачей, будут созданы методом Wait, даже если метод Wait был вызван после завершения задачи.

Дополнительные сведения см. в разделе Практическое руководство. Ожидание завершения выполнения одной или нескольких задач.

Обработка исключений в задачах

Если задача создает одно или несколько исключений, они заключаются в AggregateException. Это исключение распространяется назад в поток, который соединяется с задачей и обычно является потоком, ожидающим задачу или выполняющим попытки получить доступ к свойству Result задачи. Такое поведение служит для принудительного выполнения политики платформы .NET Framework в отношении того, что все необработанные исключения по умолчанию должны отменять процесс. Вызывающий код может обрабатывать исключения с помощью метода Wait, WaitAll или WaitAny либо свойства Result() в задаче или группе задач, заключив метод Wait в блок try-catch.

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

Дополнительные сведения об исключениях и задачах см. в разделах Обработка исключений (библиотека параллельных задач) и Практическое руководство. Обработка исключений, создаваемых задачами.

Отмена задач

Класс Task поддерживает совместную отмену и полностью интегрирован с классами System.Threading.CancellationTokenSource и System.Threading.CancellationToken, новшества .NET Framework версии 4. Большинство конструкторов в классе System.Threading.Tasks.Task принимают CancellationToken как входной параметр. Большинство перегрузок StartNew также принимают CancellationToken.

Можно создать токен и выдать запрос отмены позднее с помощью класса CancellationTokenSource. Передайте токен Task в качестве аргумента и ссылайтесь на тот же токен в пользовательском делегате, который не отвечает на запрос отмены. Дополнительные сведения см. в разделах Отмена задач и Практическое руководство. Отмена задачи и ее дочерних элементов.

Класс TaskFactory

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

Класс TaskFactory по умолчанию доступен как статическое свойство класса Task или класса Task<TResult>. Кроме того, класс TaskFactory можно создать напрямую и указать различные параметры, включающие CancellationToken, параметр TaskCreationOptions, параметр TaskContinuationOptions или TaskScheduler. Любые параметры, задаваемые при создании фабрики задач, будут применены ко всем созданным задачам, если задача не будет создана с помощью перечисления TaskCreationOptions. В этом случае параметры задачи переопределяют параметры фабрики задач.

Задачи без делегатов

В некоторых случаях может потребоваться использовать Task для инкапсуляции некоторой асинхронной операции, которая выполняется внешним компонентом, а не собственным пользовательским делегатом. Если операция основана на шаблоне Begin/End модели асинхронного программирования, можно использовать методы FromAsync. В противном случае можно использовать объект TaskCompletionSource<TResult> для заключения операции в задачу, чтобы получить некоторые преимущества программирования Task, например поддержку распространения исключений и продолжений. Дополнительные сведения см. в разделе TaskCompletionSource<TResult>.

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

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

Структуры связанных данных

Библиотека параллельных задач имеет несколько новых открытых типов, которые полезны в параллельных и последовательных сценариях. Они включают несколько потокобезопасных, быстрых и масштабируемых классов коллекций в пространстве имен System.Collections.Concurrent и несколько новых типов синхронизации, например SemaphoreLock и System.Threading.ManualResetEventSlim, которые более эффективны, чем их предшественники для определенных типов рабочих нагрузок. Другие новые типы в .NET Framework версии 4, например System.Threading.Barrier и System.Threading.SpinLock, предоставляют функциональные возможности, которые не были доступны в более ранних выпусках. Дополнительные сведения см. в разделе Структуры данных для параллельного программирования.

Настраиваемые типы задач

Наследование из System.Threading.Tasks.Task или System.Threading.Tasks.Task<TResult> не рекомендуется. Вместо этого с помощью свойства AsyncState свяжите дополнительные данные или состояние с объектом Task или Task<TResult>. Можно также использовать методы расширения для расширения функциональных возможностей классов Task и Task<TResult>. Дополнительные сведения о методах расширения см. в разделах Методы расширения (Руководство по программированию в C#) и Методы расширения (Visual Basic).

Если необходимо наследовать из Task или Task<TResult>, классы System.Threading.Tasks.TaskFactory, System.Threading.Tasks.TaskFactory<TResult> или System.Threading.Tasks.TaskCompletionSource<TResult> нельзя использовать для создания экземпляров настраиваемого типа задач, поскольку эти классы создают только объекты Task и Task<TResult>. Кроме того, механизмы продолжения задачи, работу которых обеспечивают Task, Task<TResult>, TaskFactory и TaskFactory<TResult>, нельзя использовать для создания экземпляров настраиваемого типа задач, поскольку эти механизмы также создают только объекты Task и Task<TResult>.

Связанные разделы

Название

Описание

Задачи продолжения

Описание работы продолжений.

Вложенные и дочерние задачи

Описание различий между дочерними и вложенными задачами.

Отмена задач

Описание поддержки отмены, встроенной в класс Task.

Обработка исключений (библиотека параллельных задач)

Описание обработки исключений в параллельных потоках.

Практическое руководство. Использование метода Parallel.Invoke для выполнения параллельных операций

Описание использования Invoke.

Практическое руководство. Возвращение значения из задачи

Описание возврата значений из задач.

Практическое руководство. Ожидание завершения выполнения одной или нескольких задач

Описание ожидания задач.

Практическое руководство. Отмена задачи и ее дочерних элементов

Описание отмены задач.

Практическое руководство. Обработка исключений, создаваемых задачами

Описание обработки исключений, созданных задачами.

Практическое руководство. Сцепление нескольких задач с помощью продолжений

Описание выполнения задачи по завершении другой задачи.

Практическое руководство. Переход по двоичному дереву с помощью параллельных задач

Описание использования задач для прохождения двоичного дерева.

Параллелизм данных (библиотека параллельных задач)

Описывает способы использования методов For и ForEach для создания параллельных циклов для данных.

Параллельное программирование в .NET Framework

Узел верхнего уровня для параллельного программирования в .NET.

См. также

Основные понятия

Параллельное программирование в .NET Framework

Журнал изменений

Дата

Журнал

Причина

Март 2011

Добавлена информация о способах наследования из классов Task и Task<TResult>.

Улучшение информации.