Параллелизм задач (среда выполнения с параллелизмом)
В среде выполнения с параллелизмом, задача — это единица работы, выполняющая конкретные действия и обычно выполняемая параллельно с другими задачами. Задачу можно разложить на дополнительные, более мелкие задачи, организованные в группу задач.
Задачи используются при написании асинхронного кода, в котором требуется выполнение определенной операции после завершения асинхронной операции. Например, можно использовать задачу для асинхронного чтения из файла, а затем использовать другую задачу — задачу продолжения, которая рассматривается далее в этом документе — для обработки данных после того, как они будут доступны. И наоборот, группы задач можно использовать, чтобы разделить параллельную работу на меньшие части. Предположим, например, что имеется рекурсивный алгоритм, разделяющий оставшуюся работу на два раздела. Для одновременного выполнения этих разделов можно использовать группы задач и затем ждать завершения выполнения разделенной работы.
Совет
Если нужно параллельно применить одну процедуру ко всем элементам коллекции, используйте параллельный алгоритм, например concurrency::parallel_for, вместо задачи или группы задач.Дополнительные сведения об алгоритмах параллельной обработки см. в разделе Параллельные алгоритмы.
Ключевые моменты
При передаче переменных лямбда-выражению по ссылке, необходимо следить, чтобы переменная существовала до завершения задачи.
Используйте задачи (класс concurrency::task ) при написании асинхронного кода.
Используйте группы задач (класс concurrency::task_group или алгоритм concurrency::parallel_invoke), если требуется разбить параллельную работу на более мелкие фрагменты, а затем ожидать завершения выполнения этих более мелких фрагментов.
Используйте метод concurrency::task::then для создания продолжений. Продолжение — это задача, которая выполняется асинхронно после того, как другая задача завершается. Можно подключить любое количество продолжений для формирования цепочки асинхронной работы.
Выполнение продолжения на основе задачи всегда планируется по завершении предшествующей задачи, даже если предшествующая задача отменена или создает исключение.
Используйте concurrency::when_all для создания задачи, которая завершается после завершения всех членов набора задач. Используйте concurrency::when_any для создания задачи, которая завершается после завершения одного из членов набора задач.
Задачи и группы целевого назначения могут участвовать в механизме отмены (PPL) библиотеки параллельных шаблонов. Для получения дополнительной информации см. Отмена в библиотеке параллельных шаблонов.
Сведения о том, как среда выполнения обрабатывает исключения, созданные задачами и группами задач, см. Обработка исключений в среде выполнения с параллелизмом.
В этом документе
Использование лямбда-выражений
Класс task
Задачи продолжения
Сравнение продолжения на основе значения и на основе задачи
Составление задач
Функция when_all
Функция when_any
Отложенное выполнение задач
Группы задач
Сравнение task_group и structured_task_group
Пример
Надежное программирование
Использование лямбда-выражений
Благодаря своему краткому синтаксису лямбда-выражения —распространенный способ определения работы, выполняемой задачами и группами задач. Ниже приведены некоторые советы по использованию:
Поскольку задачи обычно выполняются в фоновых потоках, при записи переменных в лямбда-выражениях необходимо представлять себе время жизни объекта. При захвате переменной по значению в теле лямбда-выражения создается копия этой переменной. При захвате по ссылке копия не создается. Поэтому убедитесь, что время существования любой захватываемой посредством ссылки переменной превышает время задачи, в которой она используется.
При передаче лямбда-выражение в задачу не следует захватывать переменные, выделенные в стеке по ссылке.
Переменные, записываемые в лямбда-выражениях, должны быть явными, чтобы можно было видеть, что запись происходит по значению, а не по ссылке. По этой причине рекомендуется не использовать параметры [=] или [&] для лямбда-выражений.
Общий подход — это когда одна задача в цепочке продолжения присваивает значение переменной, а другая задача считывает эту переменную. Выполнить захват по значению невозможно, поскольку каждая задача продолжения имела бы другую копию переменной. Переменные, размещенные в стеке, нельзя также передавать по ссылке, потому что переменная может быть больше недействительна.
Чтобы решить эту проблему, используйте интеллектуальный указатель, например 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
*/
Дополнительные сведения о лямбда-выражениях см. в разделе Лямбда-выражения в C++.
[Наверх]
Класс task
Класс concurrency::task можно использовать для объединения задач в набор независимых операций. Эта модель композиции поддерживается понятием продолжений. Продолжение позволяет выполнять код, когда завершена предыдущая (предшествующая) задача. В результате предшествующей задачи передаваться в качестве входных данных с одним или несколькими задачами продолжения. Когда предшествующая задача завершается, все ожидающие ее задачи продолжения планируются для выполнения. Каждая задача продолжения получает копию результата предшествующей задачи. В свою очередь эти задачи продолжения могут также быть предшествующими задачами для других продолжений, создавая таким образом цепочку задач. Продолжения помогают создавать цепочки задач произвольной длины с определенными зависимости между задачами. Кроме того, задача может участвовать в отмене либо до запуска задачи, либо кооперативно, когда она выполняется. Дополнительные сведения об этой модели отмены см. в разделе Отмена в библиотеке параллельных шаблонов.
task является классом шаблона. Если параметр T относится к типу , в основе результата будет лежать строящийся тип. Этот тип может быть void, если задача не возвращает значение. T не может использовать модификатор const.
При создании задачи следует указать рабочую функцию, которая выполняет тело задачи. Эта рабочая функция может быть лямбда-функцией, указателем на функцию или объектом функции. Чтобы дождаться завершения задачи без получения результата, вызовите метод concurrency::task::wait. Метод task::wait возвращает значение concurrency::task_status, которое описывает была завершена ли задача или отменена. Чтобы получить результат задачи, вызовите метод concurrency::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
*/
При использовании функции concurrency::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, concurrency::task_completion_event, см. в разделе Пошаговое руководство. Подключение с использованием задач и HTTP-запросов XML. (Класс task_completion_event описан далее в этом документе.)
Совет
Дополнительные сведения, относящиеся к конкретным задачам в приложениях Магазин Windows, см. в разделах Asynchronous programming in C++ и Создание асинхронных операций в C++ для приложений для Магазина Windows.
[Наверх]
Задачи продолжения
В асинхронном программировании очень часто одна асинхронная операция по завершении вызывает вторую операцию и передает в нее данные. Обычно это выполняется с помощью методов обратного вызова. В среде выполнения с параллелизмом такую же функциональную возможность обеспечивают задачи продолжения. Задача продолжения (также известная как продолжение) — это асинхронная задача, вызываемая другой предшествующей задачей, при ее завершении. Использование продолжения дает следующие возможности.
Передать данные из предшествующей задачи в продолжение.
Укажите точные условия, при которых продолжение будет или не будет вызываться.
Отменить продолжение либо до его запуска, либо совместно во время его выполнения.
Предоставьте подсказки о планировании продолжения. (Этот раздел применим только при работе с приложениями Магазин Windows. Дополнительные сведения см. в разделе Создание асинхронных операций в C++ для приложений для Магазина Windows.)
Вызов нескольких продолжений из одной и той же предшествующей задачи.
Вызов одного продолжение по завершении всех или какой-либо из нескольких предшествующих задач.
Последовательно соединить продолжения в цепь любой длины.
Использовать продолжение для обработки исключений, вызванных предшествующей задачей.
Эти функции позволяют выполнить одну или несколько задач, если первая задача завершается. Например, можно создать продолжение, которое сжимает файл после того, как первая задача считывает его с диска.
Следующий пример меняет предыдущий, чтобы использовать метод concurrency::task::then для планирования продолжения, которое печатает значение предшествующей задачи, если доступно.
// 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
*/
Продолжение может также возвращать другую задачу. Если отмена не происходит, эта задача выполняется перед последующим продолжением. Эта методика называется асинхронным извлечением из оболочки. Асинхронным извлечение из оболочки удобно пользоваться, когда требуется выполнить дополнительную работу в фоновом режиме, однако нужно, чтобы текущая задача не блокировала текущий поток. (Это общее в приложениях Магазин Windows, где продолжения могут выполняться в потоке пользовательского интерфейса.) В следующем примере кода показаны три задачи. Первая задача возвращает другой задачи, выполняемой задачей до продолжения.
// 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 создает исключение concurrency::task_canceled. Если предшествующая задача выдала исключение, task::get повторно выдает это исключение. Продолжение на основе задачи не помечается как отмененное, когда его предшествующая задача отменяется.
[Наверх]
Составление задач
В этом разделе описаны функции concurrency::when_all и concurrency::when_any, которые помогают объединять несколько задач для реализации общих шаблонов.
Функция when_all
Функция when_all создает задачу завершения после набора задач заполнен. Эта функция возвращает объект std::vector, который содержит результат каждой задачи в наборе. В следующем базовом примере when_all используется для создания задача, которая представляет завершение 3 других задач.
// 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.
}
});
});
}
Рассмотрим приложение Магазин Windows, которое использует 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.В отличие от task::wait, эту функцию можно безопасно вызывать в приложении Магазин Windows в потоке ASTA (STA приложений).
[Наверх]
Функция when_any
Функция when_any создает задачу завершения после набора задач заполнен. Эта функция возвращает объект std::pair, который содержит результат задачи, и индекса этой задачи в наборе.
Функция when_any особенно полезна в следующих ситуациях.
Избыточные операции. Рассмотрим алгоритм или операцию, которые можно выполнить несколькими способами. Функцию when_any можно использовать для выбора операции, завершающейся первой, и последующей отмены оставшихся операций.
Операции с чередованием. Можно запустить несколько операций, которые все должны завершиться, и использовать функцию when_any для обработки результатов при завершении каждой операции. После завершения одной операции можно запустить одну или несколько дополнительных задач.
Регулируемые операции. Функцию when_any можно использовать для расширения предыдущего сценария путем ограничения количества одновременно выполняемых операций.
Операции с истекшим сроком действия. Функцию when_any можно использовать, чтобы сделать выбор между одной или несколькими задачами и задачей, завершающейся через определенный период времени.
Как и в случае с when_all, распространено использование продолжения с when_any для выполнения действия по завершении первой задачи из набора задач. В следующем базовом примере when_any используется для создания задача, которая представляет завершение 3 других задач.
// 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 является неблокирующим и может безопасно вызываться в приложении Магазин Windows в потоке ASTA.
[Наверх]
Отложенное выполнение задач
Иногда бывает необходимо отложить выполнение задачи до тех пор, пока не будет выполняться условие, или запустить задачу в ответ на внешнее событие. Например, в асинхронном программировании может понадобиться начать задачу в ответ на событие завершения ввода-вывода.
Этого можно достигнуть двумя способами: использовать продолжение или запустить задачу и ожидать события внутри рабочей функции задачи. Однако существуют случае, когда использование одного из этих методов невозможно. Например, чтобы создать продолжение, необходимо иметь предшествующую задачу. Однако при отсутствии предшествующей задачи можно создать событие завершения задачи и впоследствии присоединить это событие завершения к предшествующей задаче, когда она появится. Кроме того, поскольку ожидающая задача также блокирует поток, события завершения задачи можно использовать для выполнения работы при завершении асинхронной операции и, следовательно, освобождении потока.
С помощью класса concurrency::task_completion_event упрощается композиция задач. Как и класс task, параметр типа T — это тип результата, созданный задачей. Этот тип может быть void, если задача не возвращает значение. T не может использовать модификатор const. Обычно объект task_completion_event предоставляется потоку или задаче, которая сообщит, когда его значение станет доступным. В то же время одна или несколько задач заданы как прослушиватели этого события. Когда событие установлено, задачи прослушивателя завершаются и их продолжения планируются для запуска.
Пример, где task_completion_event используется для реализации задачи, которая завершается после задержки, см. в разделе Практическое руководство. Создание задачи, выполняемой после задержки.
[Наверх]
Группы задач
Группа задач упорядочивает коллекцию задач. Группы задач помещают задачи в очередь переноса нагрузки. Планировщик удаляет задачи из этой очереди и выполняет их с использованием доступных вычислительных ресурсов. После добавления задач в группу можно ожидать выполнения всех задач или отменять не начатые задачи.
PPL использует классы concurrency::task_group и concurrency::structured_task_group, чтобы представлять группы задач, и класс concurrency::task_handle — чтобы представлять задачи, выполняемые в этих группах. Класс task_handle содержит код, выполняющий работу. Как и класс task, рабочая функция существует в форме лямбда-функции, указателя функции или функционального объекта. Непосредственно с объектами task_handle обычно работать не нужно. Вместо этого можно передавать рабочие функции группе задач, которая создает объекты task_handle и управляет ими.
PPL разделяет группы задач на две категории: неструктурированные группы задач и структурированные группы задач. В PPL класс task_group используется для представления неструктурированных групп задач, а класс structured_task_group — структурированных групп задач.
Важно!
PPL также определяет алгоритм concurrency::parallel_invoke, который использует класс structured_task_group, чтобы параллельно выполнять набор задач.Так как у алгоритма parallel_invoke более сжатый синтаксис, рекомендуется при возможности использовать его, а не класс structured_task_group.Алгоритм parallel_invoke более подробно описан в разделе Параллельные алгоритмы.
Алгоритм parallel_invoke используется при наличии нескольких независимых задач, которые нужно выполнять одновременно, а перед продолжением необходимо дождаться завершения всех задач. Этот метод часто называется параллелизмом с разветвлением и соединением. Алгоритм task_group используется при наличии нескольких независимых задач, которые нужно выполнять одновременно, но дожидаться завершения задач необходимо позднее. Например, можно добавить задачи в объект task_group и дождаться завершения задач в другой функции или потоке.
Группы задач поддерживают принцип отмены. Отмена позволяет сообщить всем активным задачам, что необходимо отменить общую операцию. Отмена также предотвращает запуск еще не начатых задач. Дополнительные сведения об отмене см. в разделе Отмена в библиотеке параллельных шаблонов.
Среда выполнения также предоставляет модель обработки исключений, которая позволяет создать исключение из задачи и обработать его, если необходимо завершить связанную группу задач. Дополнительные сведения о модели обработки исключений см. в разделе Обработка исключений в среде выполнения с параллелизмом.
[Наверх]
Сравнение 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 должны происходить в одном потоке. Исключение из этого правила — методы concurrency::structured_task_group::cancel и concurrency::structured_task_group::is_canceling. Дочерняя задача может вызывать эти методы для отмены родительской группы задач или проверки отмены в любой момент времени.
В объекте task_group можно выполнять дополнительные задачи после вызова метода concurrency::task_group::wait или concurrency::task_group::run_and_wait. И наоборот, при выполнении дополнительных задач в объекте structured_task_group после вызова метода concurrency::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. Объект task_handle создается с помощью функции concurrency::make_task, как показано в следующем основном примере.
// 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 поддерживают отмену. Дополнительные сведения об отмене см. в разделе Отмена в библиотеке параллельных шаблонов.
[Наверх]
Пример
В следующем общем примере показано, как работать с группами задач. В этом примере алгоритм 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();
}
Ниже приведен пример выходных данных для данного примера.
Поскольку алгоритм parallel_invoke выполняет задачи параллельно, порядок выходных сообщений может меняться.
Полные примеры, демонстрирующие использование алгоритма parallel_invoke, см. в разделах Практическое руководство. Использование функции parallel_invoke для написания программы параллельной сортировки и Практическое руководство. Использование функции parallel_invoke для выполнения параллельных операций. Полный пример использования класса Walkthrough: Implementing Futures для реализации асинхронных фьючерсов см. в разделе Пошаговое руководство. Реализация фьючерсов.
[Наверх]
Надежное программирование
Обязательно разберитесь в ролях отмены и обработки исключений при использовании задач. групп задач и параллельных алгоритмов. Например, в дереве параллельной работы отмененная задача не позволяет выполняться дочерним задачам. Это может привести к проблемам, если одна из дочерних задач выполняет операцию, важную для приложения, например высвобождает ресурс. Кроме того, если дочерняя задача создает исключение, оно может распространиться через деструктор объекта и вызвать неопределенное поведение приложения. Пример, иллюстрирующий эти моменты, см. в разделе Understand how Cancellation and Exception Handling Affect Object Destruction документа "Best Practices in the Parallel Patterns Library". Дополнительные сведения о моделях отмены и обработки исключений в PPL см. в разделах Отмена в библиотеке параллельных шаблонов и Обработка исключений в среде выполнения с параллелизмом.
[Наверх]
Связанные разделы
Название |
Описание |
---|---|
Показывает использование алгоритма parallel_invoke для повышения производительности алгоритма битонной сортировки. |
|
Практическое руководство. Использование функции parallel_invoke для выполнения параллельных операций |
Показывает использование алгоритма parallel_invoke для улучшения производительности программы, выполняющей несколько операций с общим источником данных. |
Практическое руководство. Создание задачи, выполняемой после задержки |
Показывает, как использовать классы task, cancellation_token_source, cancellation_token и task_completion_event для создания задачи, завершающейся после задержки. |
Показывает объединение существующих функциональных возможностей в среде выполнения с параллелизмом для создания дополнительных функциональных возможностей. |
|
Описывает библиотеку PPL, которая предоставляет императивную модель программирования для разработки параллельных приложений. |