Параллелизм задач (среда выполнения с параллелизмом)
В среде выполнения параллелизма задача — это единица работы, которая выполняет определенное задание и обычно выполняется параллельно с другими задачами. Задачу можно разложить на дополнительные, более подробные задачи, организованные в группу задач.
Задачи используются, когда при создании асинхронного кода требуется, чтобы после завершения асинхронной операции выполнялись некоторые операции. Например, можно использовать задачу для асинхронного чтения из файла, а затем использовать другую задачу - задачу продолжения, которая описана далее в этом документе, чтобы обработать данные после того, как он станет доступным. И наоборот, можно использовать группы задач для разбиения параллельной работы на более мелкие части. Например, предположим, что имеется рекурсивный алгоритм, разделяющий оставшуюся работу на два раздела. С помощью групп задач вы можете одновременно запустить эти разделы, а затем подождать, пока выполнится эта разделенная задача.
Совет
Если вы хотите применить одну подпрограмму к каждому элементу коллекции параллельно, используйте параллельный алгоритм, например параллелизм::p arallel_for, а не задачу или группу задач. Дополнительные сведения о параллельных алгоритмах см. в разделе "Параллельные алгоритмы".
Основные моменты
При передаче переменных в лямбда-выражение по ссылке необходимо обеспечить сохранение существования этой переменной до завершения задачи.
Используйте задачи ( класс параллелизма::task ) при написании асинхронного кода. Класс задач использует в качестве своего планировщика Windows ThreadPool (пул потоков Windows), а не среду выполнения с параллелизмом.
Используйте группы задач (класс параллелизма::task_group или алгоритм параллелизма::p arallel_invoke), если требуется разложить параллельную работу на небольшие части, а затем дождитесь завершения этих небольших частей.
Используйте метод параллелизма::task::then для создания продолжения. Продолжение — это задача, которая выполняется асинхронно после завершения другой задачи. Вы можете подключать любое количество продолжений для формирования цепочки асинхронной работы.
Продолжение на основе задачи всегда планируется для выполнения после завершения предшествующей задачи, даже если предшествующая задача отменяется или создает исключение.
Используйте параллелизм::when_all , чтобы создать задачу, которая завершается после завершения каждого члена набора задач. Используйте параллелизм::when_any , чтобы создать задачу, которая завершается после завершения одного члена набора задач.
Задачи и группы задач могут участвовать в механизме отмены библиотеки параллельных шаблонов (PPL). Дополнительные сведения см. в разделе "Отмена" в PPL.
Сведения о том, как среда выполнения обрабатывает исключения, создаваемые задачами и группами задач, см. в разделе "Обработка исключений".
В этом документе
Использование лямбда-выражений
Благодаря их лаконичному синтаксису лямбда-выражения часто используют для определения операций, выполняемых задачами и группами задач. Ниже приведены некоторые советы по использованию.
Поскольку задачи обычно выполняются в фоновых потоках, помните о времени существования объекта при включении переменных в лямбда-выражения. При вводе переменной по значению в тексте лямбда-выражения создается копия этой переменной. При вводе по ссылке копия не создается. Следовательно, необходимо убедиться, что время существования любой введенной по ссылке переменной превышает время существования задачи, которая ее использует.
При передаче лямбда-выражения задаче не фиксируйте переменные, выделенные в стеке по ссылке.
Будьте явными в лямбда-выражениях, чтобы определить, что вы фиксируете по значению и по ссылке. По этой причине рекомендуется не использовать параметры
[=]
или[&]
для лямбда-выражений.
Распространенный подход заключается в том, что одна задача в цепочке продолжения назначает переменную, а другая задача читает эту переменную. Невозможно записать по значению, так как каждая задача продолжения будет содержать другую копию переменной. Для переменных, выделенных стеком, вы также не можете записать по ссылке, так как переменная больше не может быть допустимой.
Чтобы решить эту проблему, используйте умный указатель, например std::shared_ptr, для упаковки переменной и передачи интеллектуального указателя по значению. Таким образом этот базовый объект можно назначать и читать, и срок его существования будет превышать срок существования задач, которые его используют. Используйте этот метод даже в том случае, если переменная является указателем или дескриптором с подсчетом ссылок (^
) объекта среды выполнения Windows. Простой пример:
// lambda-task-lifetime.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
#include <string>
using namespace concurrency;
using namespace std;
task<wstring> write_to_string()
{
// Create a shared pointer to a string that is
// assigned to and read by multiple tasks.
// By using a shared pointer, the string outlives
// the tasks, which can run in the background after
// this function exits.
auto s = make_shared<wstring>(L"Value 1");
return create_task([s]
{
// Print the current value.
wcout << L"Current value: " << *s << endl;
// Assign to a new value.
*s = L"Value 2";
}).then([s]
{
// Print the current value.
wcout << L"Current value: " << *s << endl;
// Assign to a new value and return the string.
*s = L"Value 3";
return *s;
});
}
int wmain()
{
// Create a chain of tasks that work with a string.
auto t = write_to_string();
// Wait for the tasks to finish and print the result.
wcout << L"Final value: " << t.get() << endl;
}
/* Output:
Current value: Value 1
Current value: Value 2
Final value: Value 3
*/
Дополнительные сведения о лямбда-выражениях см. в разделе Лямбда-выражения.
Класс задачи
Класс concurrency::task можно использовать для создания задач в набор зависимых операций. Эта модель композиции поддерживается понятием продолжения. Продолжение позволяет выполнять код, когда предыдущая или предшествующая задача завершается. Результат предшествующей задачи передается в качестве входных данных в одну или несколько задач продолжения. По завершении предшествующей задачи все ожидающие ее задачи продолжения планируются для выполнения. Каждая задача продолжения получает копию результатов предшествующей задачи. В свою очередь, эти задачи продолжения также могут быть предшествующими задачами для других продолжений, тем самым создавая цепочки задач. Продолжения помогают создавать цепочки задач произвольной длины с определенными зависимостями между входящими в них задачами. Кроме того, задача может участвовать в отмене либо до запуска, либо совместно во время выполнения. Дополнительные сведения об этой модели отмены см. в разделе "Отмена" в PPL.
task
является классом шаблона. Параметр типа T
— это тип результата, созданного задачей. Это может быть тип void
, если задача не возвращает значение. Параметр T
не может использовать модификатор const
.
При создании задачи вы предоставляете рабочую функцию , которая выполняет текст задачи. Эта рабочая функция поступает в виде лямбда-функции, указателя функции или объекта функции. Чтобы дождаться завершения задачи без получения результата, вызовите метод параллелизма::task::wait . Метод task::wait
возвращает значение параллелизма::task_status , описывающее, была ли задача завершена или отменена. Чтобы получить результат задачи, вызовите метод параллелизма::task::get . Этот метод вызывает метод task::wait
для ожидания завершения задачи и таким образом блокирует выполнение текущего потока, пока не станет доступен результат.
В следующем примере показано, как создать задачу, дождаться ее результата и отобразить полученное значение. В примерах в этой документации используются лямбда-функции, поскольку они обеспечивают более лаконичный синтаксис. Однако при использовании задач вы также можете применять указатели функций и объекты функций.
// basic-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
// Create a task.
task<int> t([]()
{
return 42;
});
// In this example, you don't necessarily need to call wait() because
// the call to get() also waits for the result.
t.wait();
// Print the result.
wcout << t.get() << endl;
}
/* Output:
42
*/
При использовании функции параллелизма::create_task можно использовать auto
ключевое слово вместо объявления типа. Например, рассмотрим следующий код, который создает и печатает матрицу тождественности.
// create-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <string>
#include <iostream>
#include <array>
using namespace concurrency;
using namespace std;
int wmain()
{
task<array<array<int, 10>, 10>> create_identity_matrix([]
{
array<array<int, 10>, 10> matrix;
int row = 0;
for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow)
{
fill(begin(matrixRow), end(matrixRow), 0);
matrixRow[row] = 1;
row++;
});
return matrix;
});
auto print_matrix = create_identity_matrix.then([](array<array<int, 10>, 10> matrix)
{
for_each(begin(matrix), end(matrix), [](array<int, 10>& matrixRow)
{
wstring comma;
for_each(begin(matrixRow), end(matrixRow), [&comma](int n)
{
wcout << comma << n;
comma = L", ";
});
wcout << endl;
});
});
print_matrix.wait();
}
/* Output:
1, 0, 0, 0, 0, 0, 0, 0, 0, 0
0, 1, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 1, 0, 0, 0, 0, 0, 0, 0
0, 0, 0, 1, 0, 0, 0, 0, 0, 0
0, 0, 0, 0, 1, 0, 0, 0, 0, 0
0, 0, 0, 0, 0, 1, 0, 0, 0, 0
0, 0, 0, 0, 0, 0, 1, 0, 0, 0
0, 0, 0, 0, 0, 0, 0, 1, 0, 0
0, 0, 0, 0, 0, 0, 0, 0, 1, 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 1
*/
Вы можете использовать функцию create_task
для создания эквивалентной операции.
auto create_identity_matrix = create_task([]
{
array<array<int, 10>, 10> matrix;
int row = 0;
for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow)
{
fill(begin(matrixRow), end(matrixRow), 0);
matrixRow[row] = 1;
row++;
});
return matrix;
});
Если во время выполнения задачи возникает исключение, среда выполнения маршалирует исключение в последующий вызов метода task::get
или task::wait
, или в продолжение на основе задачи. Дополнительные сведения о механизме обработки исключений задачи см. в разделе "Обработка исключений".
Пример использования task
параллелизма ::task_completion_event, отмены см. в пошаговом руководстве. Подключение с помощью задач и XML-запросов HTTP. (Класс task_completion_event
описывается далее в этом документе.)
Совет
Дополнительные сведения, относящиеся к задачам в приложениях UWP, см. в статье асинхронное программирование в C++ и создание асинхронных операций в C++ для приложений UWP.
Задачи продолжения
В асинхронном программировании очень распространено при завершении одной асинхронной операции вызывать вторую операцию и передавать в нее данные. Как правило, это делается с помощью методов обратного вызова. В среде выполнения параллелизма те же функции предоставляются задачами продолжения. Задача продолжения (также известная как продолжение) — это асинхронная задача, вызываемая другой задачей, которая называется отступом, когда выполняется отступ. С помощью продолжений вы можете делать следующее.
Передавать данные из предшествующей задачи в продолжение.
Указывать точные условия, при которых продолжение вызывается или не вызывается.
Отменять продолжение перед его запуском либо совместно во время его выполнения.
Определять подсказки, как должно планироваться продолжение. (Это относится только к приложениям универсальная платформа Windows (UWP). Дополнительные сведения см. в разделе "Создание асинхронных операций в C++ для приложений UWP".)
Вызывать несколько продолжений из одной и той же предшествующей задачи.
Вызывать одно продолжение по завершении всех или одной из нескольких предшествующих задач.
Прикреплять продолжения одно после другого до любой длины.
Использовать продолжение для обработки исключений, вызванных предшествующей задачей.
Эти возможности позволяют выполнять одну или несколько задач после завершения первой задачи. Например, можно создать продолжение, которое сжимает файл после того, как первая задача прочитает этот файл с диска.
Следующий пример изменяет предыдущий, чтобы использовать параллелизм::task::then , чтобы запланировать продолжение, которое выводит значение задачи antecedent при его доступности.
// basic-continuation.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
auto t = create_task([]() -> int
{
return 42;
});
t.then([](int result)
{
wcout << result << endl;
}).wait();
// Alternatively, you can chain the tasks directly and
// eliminate the local variable.
/*create_task([]() -> int
{
return 42;
}).then([](int result)
{
wcout << result << endl;
}).wait();*/
}
/* Output:
42
*/
Вы можете прикреплять и вкладывать задачи до любой длины. Задача также может иметь несколько продолжений. В следующем примере демонстрируется базовая цепочка продолжений, которая увеличивает значение предыдущей задачи три раза.
// continuation-chain.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
auto t = create_task([]() -> int
{
return 0;
});
// Create a lambda that increments its input value.
auto increment = [](int n) { return n + 1; };
// Run a chain of continuations and print the result.
int result = t.then(increment).then(increment).then(increment).get();
wcout << result << endl;
}
/* Output:
3
*/
Продолжение может также возвращать другую задачу. Если отмена отсутствует, то эта задача выполняется до последующего продолжения. Этот метод называется асинхронным распакуванием. Асинхронное развертывание удобно использовать, когда требуется выполнить дополнительную работу в фоновом режиме, но так, чтобы текущая задача не блокировала текущий поток. (Это распространено в приложениях UWP, где продолжение может выполняться в потоке пользовательского интерфейса). В следующем примере показаны три задачи. Первая задача возвращает вторую задачу, которая выполняется перед задачей продолжения.
// async-unwrapping.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
auto t = create_task([]()
{
wcout << L"Task A" << endl;
// Create an inner task that runs before any continuation
// of the outer task.
return create_task([]()
{
wcout << L"Task B" << endl;
});
});
// Run and wait for a continuation of the outer task.
t.then([]()
{
wcout << L"Task C" << endl;
}).wait();
}
/* Output:
Task A
Task B
Task C
*/
Внимание
Когда продолжение задачи возвращает вложенную задачу типа N
, результирующая задача имеет тип N
, а не task<N>
, и завершается при завершении вложенной задачи. Другими словами, продолжение выполняет развертывание вложенной задачи.
Продолжение на основе значений и продолжение на основе задач
Принимая во внимание объект task
, который имеет возвращаемый тип T
, вы можете предоставить значение типа T
или task<T>
в соответствующие задачи продолжения. Продолжение, которое принимает типT
, называется продолжением на основе значений. Продолжение на основе значения планируется для выполнения, когда предшествующая задача завершается без ошибок и не отменяется. Продолжение, которое принимает тип task<T>
как его параметр, называется продолжением на основе задач. Продолжение на основе задачи всегда планируется для выполнения после завершения предшествующей задачи, даже если предшествующая задача отменяется или создает исключение. Затем можно вызвать task::get
, чтобы получить результат предшествующей задачи. Если задача была отменена, task::get
вызывает параллелизм::task_canceled. Если предшествующая задача выдала исключение, task::get
повторно выдает это исключение. Продолжение на основе задачи не отмечается как отмененное, когда отменяется предшествующая задача.
Создание задач
В этом разделе описаны функции параллелизма::when_all и параллелизма::when_any, которые помогают создавать несколько задач для реализации общих шаблонов.
Функция when_all
Функция when_all
создает задачу, которая выполняется после завершения набора задач. Эта функция возвращает объект std::vector , содержащий результат каждой задачи в наборе. В следующем базовом примере функция when_all
используется для создания задачи, которая представляет завершение трех других задач.
// join-tasks.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
// Start multiple tasks.
array<task<void>, 3> tasks =
{
create_task([] { wcout << L"Hello from taskA." << endl; }),
create_task([] { wcout << L"Hello from taskB." << endl; }),
create_task([] { wcout << L"Hello from taskC." << endl; })
};
auto joinTask = when_all(begin(tasks), end(tasks));
// Print a message from the joining thread.
wcout << L"Hello from the joining thread." << endl;
// Wait for the tasks to finish.
joinTask.wait();
}
/* Sample output:
Hello from the joining thread.
Hello from taskA.
Hello from taskC.
Hello from taskB.
*/
Примечание.
Задачи, которые вы передаете в функцию when_all
, должны быть единообразными. Другими словами, все они должны возвращать один и тот же тип.
Для создания задачи, выполняемой после завершения набора задач, можно также использовать синтаксис &&
, как показано в следующем примере.
auto t = t1 && t2; // same as when_all
Обычно для выполнения действия после завершения набора задач используется продолжение вместе с функцией when_all
. В следующем примере изменяется предыдущий пример для печати суммы трех задач, каждая из которых создает результат int
.
// Start multiple tasks.
array<task<int>, 3> tasks =
{
create_task([]() -> int { return 88; }),
create_task([]() -> int { return 42; }),
create_task([]() -> int { return 99; })
};
auto joinTask = when_all(begin(tasks), end(tasks)).then([](vector<int> results)
{
wcout << L"The sum is "
<< accumulate(begin(results), end(results), 0)
<< L'.' << endl;
});
// Print a message from the joining thread.
wcout << L"Hello from the joining thread." << endl;
// Wait for the tasks to finish.
joinTask.wait();
/* Output:
Hello from the joining thread.
The sum is 229.
*/
В этом примере можно также указать task<vector<int>>
, чтобы создать продолжение на основе задачи.
Если какая-либо из задач в наборе задач отменяется или порождает исключение, when_all
немедленно завершается и не ждет завершения выполнения оставшихся задач. Если выдается исключение, среда выполнения повторно выдает это исключение при вызове task::get
или task::wait
в объекте задачи, который возвращает when_all
. Если исключение выдают несколько задач, среда выполнения выбирает одну из них. Поэтому убедитесь, что вы заметили все исключения после завершения всех задач; необработанное исключение задачи приведет к завершению работы приложения.
Ниже приведена служебная функция, которую можно использовать для обеспечения того, чтобы программа наблюдала за всеми исключениями. Для каждой задачи в указанном диапазоне эта служебная функция observe_all_exceptions
запускает повторную выдачу каждого возникшего исключения, а затем поглощает это исключение.
// Observes all exceptions that occurred in all tasks in the given range.
template<class T, class InIt>
void observe_all_exceptions(InIt first, InIt last)
{
std::for_each(first, last, [](concurrency::task<T> t)
{
t.then([](concurrency::task<T> previousTask)
{
try
{
previousTask.get();
}
// Although you could catch (...), this demonstrates how to catch specific exceptions. Your app
// might handle different exception types in different ways.
catch (Platform::Exception^)
{
// Swallow the exception.
}
catch (const std::exception&)
{
// Swallow the exception.
}
});
});
}
Рассмотрим приложение UWP, использующее C++ и XAML, и записывает набор файлов на диск. В следующем примере показано, как использовать функции when_all
и observe_all_exceptions
, чтобы убедиться, что программа обнаруживает все исключения.
// Writes content to files in the provided storage folder.
// The first element in each pair is the file name. The second element holds the file contents.
task<void> MainPage::WriteFilesAsync(StorageFolder^ folder, const vector<pair<String^, String^>>& fileContents)
{
// For each file, create a task chain that creates the file and then writes content to it. Then add the task chain to a vector of tasks.
vector<task<void>> tasks;
for (auto fileContent : fileContents)
{
auto fileName = fileContent.first;
auto content = fileContent.second;
// Create the file. The CreationCollisionOption::FailIfExists flag specifies to fail if the file already exists.
tasks.emplace_back(create_task(folder->CreateFileAsync(fileName, CreationCollisionOption::FailIfExists)).then([content](StorageFile^ file)
{
// Write its contents.
return create_task(FileIO::WriteTextAsync(file, content));
}));
}
// When all tasks finish, create a continuation task that observes any exceptions that occurred.
return when_all(begin(tasks), end(tasks)).then([tasks](task<void> previousTask)
{
task_status status = completed;
try
{
status = previousTask.wait();
}
catch (COMException^ e)
{
// We'll handle the specific errors below.
}
// TODO: If other exception types might happen, add catch handlers here.
// Ensure that we observe all exceptions.
observe_all_exceptions<void>(begin(tasks), end(tasks));
// Cancel any continuations that occur after this task if any previous task was canceled.
// Although cancellation is not part of this example, we recommend this pattern for cases that do.
if (status == canceled)
{
cancel_current_task();
}
});
}
Запуск примера
- Добавьте в файл MainPage.xaml элемент управления
Button
.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
- В MainPage.xaml.h добавьте в раздел
private
объявления классаMainPage
следующие предваряющие объявления.
void Button_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
concurrency::task<void> WriteFilesAsync(Windows::Storage::StorageFolder^ folder, const std::vector<std::pair<Platform::String^, Platform::String^>>& fileContents);
- В MainPage.xaml.cpp реализуйте обработчик событий
Button_Click
.
// A button click handler that demonstrates the scenario.
void MainPage::Button_Click(Object^ sender, RoutedEventArgs^ e)
{
// In this example, the same file name is specified two times. WriteFilesAsync fails if one of the files already exists.
vector<pair<String^, String^>> fileContents;
fileContents.emplace_back(make_pair(ref new String(L"file1.txt"), ref new String(L"Contents of file 1")));
fileContents.emplace_back(make_pair(ref new String(L"file2.txt"), ref new String(L"Contents of file 2")));
fileContents.emplace_back(make_pair(ref new String(L"file1.txt"), ref new String(L"Contents of file 3")));
Button1->IsEnabled = false; // Disable the button during the operation.
WriteFilesAsync(ApplicationData::Current->TemporaryFolder, fileContents).then([this](task<void> previousTask)
{
try
{
previousTask.get();
}
// Although cancellation is not part of this example, we recommend this pattern for cases that do.
catch (const task_canceled&)
{
// Your app might show a message to the user, or handle the error in some other way.
}
Button1->IsEnabled = true; // Enable the button.
});
}
- В MainPage.xaml.cpp реализуйте
WriteFilesAsync
, как показано в примере.
Совет
Функция when_all
является функцией без блокировки, в качестве результата создающей task
. В отличие от задачи::wait, это безопасно вызывать эту функцию в приложении UWP в потоке ASTA (Application STA).
Функция when_any
Функция when_any
создает задачу, которая выполняется после завершения первой задачи в наборе задач. Эта функция возвращает объект std::p air , содержащий результат завершенной задачи и индекс этой задачи в наборе.
Функция when_any
особенно полезна в следующих ситуациях.
Избыточные операции. Рассмотрим алгоритм или операцию, которые можно выполнить несколькими способами. Функцию
when_any
можно использовать для выбора операции, которая завершается первой, и последующей отмены оставшихся операций.Операции с чередованием. Можно запустить несколько операций, которые все должны завершиться, и использовать функцию
when_any
для обработки результатов при завершении каждой операции. После завершения одной операции можно запустить одну или несколько дополнительных задач.Регулируемые операции. Функцию
when_any
можно использовать для расширения предыдущего сценария путем ограничения количества параллельных операций.Операции с истекшим сроком действия. Функцию
when_any
можно использовать, чтобы сделать выбор между одной или несколькими задачами и задачей, завершающейся после определенного времени.
Как и в случае с функцией when_all
, обычно для выполнения действия после завершения первой задачи в наборе используется продолжение, имеющее функцию when_any
. В следующем базовом примере функция when_any
используется для создания задачи, которая выполняется после завершения первой из трех других задач.
// select-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
// Start multiple tasks.
array<task<int>, 3> tasks = {
create_task([]() -> int { return 88; }),
create_task([]() -> int { return 42; }),
create_task([]() -> int { return 99; })
};
// Select the first to finish.
when_any(begin(tasks), end(tasks)).then([](pair<int, size_t> result)
{
wcout << "First task to finish returns "
<< result.first
<< L" and has index "
<< result.second
<< L'.' << endl;
}).wait();
}
/* Sample output:
First task to finish returns 42 and has index 1.
*/
В этом примере можно также указать task<pair<int, size_t>>
, чтобы создать продолжение на основе задачи.
Примечание.
Как и при использовании функции when_all
, задачи, которые вы передаете в when_any
, должны возвращать один и тот же тип.
Для создания задачи, выполняемой после завершения первой задачи в наборе, можно также использовать синтаксис ||
, как показано в следующем примере.
auto t = t1 || t2; // same as when_any
Совет
Как и в случае when_all
с, when_any
не блокируется и безопасно вызывать приложение UWP в потоке ASTA.
Задержка выполнения задачи
Иногда требуется отложить выполнение задачи до выполнения условия или запустить задачу в ответ на внешнее событие. Например, в асинхронном программировании может потребоваться запустить задачу в ответ на событие завершения операции ввода-вывода.
Это можно сделать двумя способами: использовать продолжение или запустить задачу и ждать события внутри рабочей функции задачи. Однако бывают случаи, когда невозможно использовать ни один из этих способов. Например, чтобы создать продолжение, необходимо иметь предшествующую задачу. Однако если у вас нет предшествующей задачи, вы можете создать событие завершения задачи и более поздние цепочки, что событие завершения для задачи antecedent, когда она станет доступной. Кроме того, поскольку ожидающая задача также блокирует поток, можно использовать события завершения задачи для выполнения работы при завершении асинхронной операции и тем самым освободить поток.
Класс параллелизма::task_completion_event помогает упростить такую композицию задач. Как и в классе task
, параметр типа T
— это тип результата, созданного задачей. Это может быть тип void
, если задача не возвращает значение. Параметр T
не может использовать модификатор const
. Как правило, объект task_completion_event
передается в поток или задачу, которые будут сообщать, когда значение для них станет доступным. В то же время одна или несколько задач устанавливаются в качестве прослушивателей этого события. Когда событие возникает, задачи прослушивателя выполняются и их продолжения планируются для запуска.
Пример, который используется task_completion_event
для реализации задачи, завершающейся после задержки, см. в разделе "Практическое руководство. Создание задачи, завершающейся после задержки".
Группы задач
Группа задач упорядочивает коллекцию задач. Группы задач помещают задачи в очередь перехвата работы. Планировщик удаляет задачи из этой очереди и выполняет их с использованием доступных вычислительных ресурсов. После добавления задач в группу задач можно ожидать завершения всех задач или отменить задачи, которые еще не запускались.
PPL использует классы параллелизма::task_group и параллелизма::structured_task_group для представления групп задач и класса concurrency::task_handle для представления задач, выполняемых в этих группах. Класс task_handle
инкапсулирует код, выполняющий работу. Как и в случае класса task
, эта рабочая функция поступает в виде лямбда-функции, указателя функции или объекта функции. Обычно не требуется работать с объектами task_handle
напрямую. Вместо этого вы передаете рабочие функции в группу задач, а группа задач создает объекты task_handle
и управляет ими.
PPL делит группы задач на две категории: неструктурированные группы задач и структурированные группы задач. PPL использует класс task_group
для представления неструктурированных групп задач и класс structured_task_group
— для представления структурированных групп задач.
Внимание
PPL также определяет алгоритм параллелизма::p arallel_invoke , который использует structured_task_group
класс для параллельного выполнения набора задач. Поскольку алгоритм parallel_invoke
имеет более лаконичный синтаксис, рекомендуется по возможности использовать его вместо класса structured_task_group
. В разделе Параллельные алгоритмы подробно описаныparallel_invoke
.
Используйте алгоритм parallel_invoke
, когда имеется несколько независимых задач, которые требуется выполнить одновременно, и перед продолжением необходимо дождаться завершения всех задач. Этот метод часто называется вилкой и параллелизмом соединения . Используйте алгоритм task_group
, когда имеется несколько независимых задач, которые требуется выполнить одновременно, но дождаться завершения всех задач требуется позднее. Например, вы можете добавить задачи в объект task_group
и дожидаться завершения этих задач в другой функции или из другого потока.
Группы задач поддерживают принцип отмены. Отмена позволяет сообщить всем активным задачам, что необходимо отменить всю операцию. Отмена также предотвращает запуск задач, которые еще не начали выполняться. Дополнительные сведения об отмене см. в разделе "Отмена" в PPL.
Среда выполнения также предоставляет модель обработки исключений, которая позволяет вызывать исключение из задачи и обработать это исключение при ожидании завершения группы связанных задач. Дополнительные сведения об этой модели обработки исключений см. в разделе "Обработка исключений".
Сравнение task_group с structured_task_group
Несмотря на то что мы рекомендуем использовать task_group
или parallel_invoke
вместо класса structured_task_group
, бывают ситуации, в которых вы захотите использовать класс structured_task_group
, например при создании параллельного алгоритма, который выполняет переменное количество задач или которому требуется поддержка отмены. В этом разделе описываются различия между классами task_group
и structured_task_group
.
Класс task_group
является потокобезопасным. Поэтому можно добавлять задачи в объект task_group
из нескольких потоков и ожидать или отменять объект task_group
из нескольких потоков. Создание и уничтожение объекта structured_task_group
должно происходить в одной лексической области. Кроме того, все операции с объектом structured_task_group
должны происходить в одном потоке. Исключением из этого правила является методы параллелизма::structured_task_group::cancel и параллелизма:::structured_task_group:::is_canceling . Дочерняя задача может вызывать эти методы для отмены родительской группы задач или проверки на предмет отмены в любое время.
После вызова метода concurrency::task_group::wait или concurrency:::task_group::run_and_wait можно выполнить дополнительные задачиtask_group
. И наоборот, если выполнять дополнительные задачи в structured_task_group
объекте после вызова метода параллелизма::structured_task_group::wait или concurrency::structured_task_group::run_and_wait , поведение не определено.
Поскольку класс structured_task_group
не синхронизируется в потоках, он имеет меньше затрат на выполнение, чем класс task_group
. Таким образом, если проблема не требует планирования работы в нескольких потоках и нельзя использовать алгоритм parallel_invoke
, класс structured_task_group
класс может помочь написать более производительный код.
При использовании одного объекта structured_task_group
внутри другого объекта structured_task_group
внутренний объект должен быть завершен и уничтожен до завершения внешнего объекта. Класс task_group
не требуется для завершения вложенных групп задач до завершения внешней группы.
Неструктурированные группы задач и структурированные группы задач работают с дескрипторами задач по-разному. Вы можете передавать рабочие функции непосредственно в объект task_group
; объект task_group
будет создавать дескриптор задач и управлять им. Класс structured_task_group
требует, чтобы вы управляли объектом task_handle
для каждой задачи. Каждый объект task_handle
должен оставаться допустимым в течение всего времени существования связанного объекта structured_task_group
. Используйте функцию параллелизма::make_task для создания task_handle
объекта, как показано в следующем базовом примере:
// make-task-structure.cpp
// compile with: /EHsc
#include <ppl.h>
using namespace concurrency;
int wmain()
{
// Use the make_task function to define several tasks.
auto task1 = make_task([] { /*TODO: Define the task body.*/ });
auto task2 = make_task([] { /*TODO: Define the task body.*/ });
auto task3 = make_task([] { /*TODO: Define the task body.*/ });
// Create a structured task group and run the tasks concurrently.
structured_task_group tasks;
tasks.run(task1);
tasks.run(task2);
tasks.run_and_wait(task3);
}
Для управления дескрипторами задач для случаев, когда у вас есть переменное количество задач, используйте подпрограмму выделения стека, например _malloca или класс контейнера, например std::vector.
И task_group
, и structured_task_group
поддерживают отмену. Дополнительные сведения об отмене см. в разделе "Отмена" в PPL.
Пример
В следующем базовом примере показано, как работать с группами задач. В этом примере используется алгоритм parallel_invoke
для выполнения двух задач одновременно. Каждая задача добавляет подзадачи в объект task_group
. Обратите внимание, что класс task_group
позволяет добавлять в него задачи одновременно нескольким задачам.
// using-task-groups.cpp
// compile with: /EHsc
#include <ppl.h>
#include <sstream>
#include <iostream>
using namespace concurrency;
using namespace std;
// Prints a message to the console.
template<typename T>
void print_message(T t)
{
wstringstream ss;
ss << L"Message from task: " << t << endl;
wcout << ss.str();
}
int wmain()
{
// A task_group object that can be used from multiple threads.
task_group tasks;
// Concurrently add several tasks to the task_group object.
parallel_invoke(
[&] {
// Add a few tasks to the task_group object.
tasks.run([] { print_message(L"Hello"); });
tasks.run([] { print_message(42); });
},
[&] {
// Add one additional task to the task_group object.
tasks.run([] { print_message(3.14); });
}
);
// Wait for all tasks to finish.
tasks.wait();
}
Ниже приведен пример выходных данных для данного примера.
Message from task: Hello
Message from task: 3.14
Message from task: 42
Поскольку алгоритм parallel_invoke
выполняет задачи параллельно, порядок выходных сообщений может меняться.
Полные примеры использования алгоритма parallel_invoke
см. в статье "Практическое руководство. Использование parallel_invoke для записи процедуры параллельной сортировки и практическое руководство. Использование parallel_invoke для выполнения параллельных операций. Полный пример, использующий task_group
класс для реализации асинхронных фьючерсов, см. в пошаговом руководстве. Реализация фьючерсов.
Отказоустойчивость
Убедитесь, что понимаете роль отмены и обработки исключений при использовании задач, групп задач и параллельных алгоритмов. Например, в дереве параллельной работы отмененная задача предотвращает запуск дочерних задач. Это может привести к проблемам, если одна из дочерних задач выполняет операцию, важную для приложения, например высвобождает ресурс. Кроме того, если дочерняя задача создает исключение, это исключение может распространиться через деструктор объекта и вызвать неопределенное поведение в приложении. Пример, демонстрирующий эти моменты, см . в разделе "Общие сведения о том, как отмена и обработка исключений влияют на уничтожение объектов" в документе библиотеки параллельных шаблонов. Дополнительные сведения о моделях отмены и обработки исключений в PPL см. в разделе "Отмена и обработка исключений".
См. также
Заголовок | Description |
---|---|
Практическое руководство. Использование функции parallel_invoke для написания программы параллельной сортировки | Показывается, как использовать алгоритм parallel_invoke для повышения производительности алгоритма битонной сортировки. |
Практическое руководство. Использование функции parallel_invoke для выполнения параллельных операций | Показывается, как использовать алгоритм parallel_invoke для повышения производительности программы, выполняющей несколько операций с общим источником данных. |
Практическое руководство. Создание задачи, выполняемой после задержки | Показывает, как использовать task классы cancellation_token_source cancellation_token и task_completion_event классы для создания задачи, которая завершается после задержки. |
Пошаговое руководство. Реализация фьючерсов | Показано, как объединить существующие функциональные возможности в среде выполнения с параллелизмом в то, что делает больше. |
Библиотека параллельных шаблонов | Описывается библиотека PPL, которая предоставляет императивную модель программирования для разработки параллельных приложений. |