Говорят, ассистенты голливудских режиссеров, ведающие подбором актерского состава, частенько отделываются от претендентов пренебрежительными словами «не звоните нам — мы сами вас вызовем». Но для разработчиков эта фраза описывает то, как работают многие инфраструктуры: вместо того чтобы дать программисту возможность управлять потоком выполнения для всего приложения, инфраструктура контролирует среду и запускает обратные вызовы или обработчики событий, предоставляемые программистом.
В асинхронных системах эта парадигма позволяет отделить запуск асинхронной операции от ее завершения. Программист инициирует операцию, а затем регистрирует обратный вызов, который будет запущен, когда появятся результаты. Избавившись от необходимости ожидания, вы можете заниматься другой полезной работой, пока идет выполнение операции, например обслуживать цикл обработки сообщений или запускать другие асинхронные операции. Окно замораживания, кручение в цикле ожидания и другие подобные явления станут реликтами прошлого, если вы будете строго придерживаться этого шаблона при программировании всех потенциально блокирующих операций. Ваши приложения будут — вы наверняка уже слышали об этом — быстрыми и отзывчивыми.
В Windows 8 асинхронные операции используются повсеместно, и WinRT предлагает новую модель программирования для согласованной работы с такой асинхронностью.
На рис. 1 демонстрируется базовый шаблон работы с асинхронными операциями. В этом коде C++-функция считывает строку из файла.
Рис. 1. Чтение из файла
template<typename Callback>
void ReadString(String^ fileName, Callback func)
{
StorageFolder^ item = KnownFolders::PicturesLibrary;
auto getFileOp = item->GetFileAsync(fileName);
getFileOp->Completed =
ref new AsyncOperationCompletedHandler<StorageFile^>
([=](IAsyncOperation<StorageFile^>^ operation,
AsyncStatus status)
{
auto storageFile = operation->GetResults();
auto openOp = storageFile->OpenAsync(FileAccessMode::Read);
openOp->Completed = ref new AsyncOperationCompletedHandler
<IRandomAccessStream^> ([=](IAsyncOperation<
IRandomAccessStream^>^ operation, AsyncStatus status)
{
auto istream = operation->GetResults();
auto reader = ref new DataReader(istream);
auto loadOp = reader->LoadAsync(istream->Size);
loadOp->Completed =
ref new AsyncOperationCompletedHandler<UINT>
([=](IAsyncOperation<UINT>^ operation,
AsyncStatus status)
{
auto bytesRead = operation->GetResults();
auto str = reader->ReadString(bytesRead);
func(str);
});
});
});
}
Первым делом обратите внимание на то, что тип значения, возвращаемого функцией ReadString, — void. Все правильно: эта функция не возвращает значение; вместо этого она принимает обратный вызов и запускает его, когда становится доступным результат. Добро пожаловать в мир асинхронного программирования: не звоните нам — мы сами вас вызовем!
Анатомия асинхронной операции WinRT
В основе асинхронности в WinRT лежат четыре интерфейса, определенные в пространстве имен Windows::Foundation: IAsyncOperation, IAsyncAction, IAsyncOperationWithProgress и IAsyncActionWithProgress. Все потенциально блокирующие или длительно выполняемые операции в WinRT определены как асинхронные. По соглашению, имя метода заканчивается на «Async», а возвращаемый тип соответствует одному из этих четырех интерфейсов. Таков метод GetFileAsync на рис. 1, который возвращает IAsyncOperation<StorageFile^>. Многие асинхронные операции не возвращают значения, и их тип — IAsyncAction. Операции, способные уведомлять о своем прогрессе, предоставляются через IAsyncOperationWithProgress и IAsyncActionWithProgress.
Чтобы указать обратный вызов завершения для асинхронной операции, задается свойство Completed. Это свойство является делегатом, который принимает асинхронный интерфейс и состояние завершения. Хотя этот делегат можно создать с помощью указателя на функцию, чаще всего вы будете пользоваться лямбдой (полагаю, что на сегодняшний день вы уже знакомы с этой частью C++11).
Очень часто вы будете обнаруживать, что используете несколько асинхронных операций совместно.
Чтобы получить значение операции, вы вызываете метод GetResults интерфейса. Заметьте: хотя это тот же интерфейс, который возвращается из вызова GetFileAsync, в обработчике завершения можно вызвать только GetResults этого интерфейса.
Второй параметр делегата завершения — AsyncStatus, который возвращает состояние операции. В реальном приложении вы проверили бы его значение перед вызовом GetResults. На рис. 1 я опустил эту часть для краткости.
Очень часто вы будете обнаруживать, что используете несколько асинхронных операций совместно. В своем примере я сначала получаю экземпляр StorageFile (вызовом GetFileAsync), затем открываю его, используя OpenAsync и получая IInputStream. Далее я загружаю данные (LoadAsync) и читаю их с помощью DataReader. Наконец, я получаю строку и вызываю переданную пользователем функцию обратного вызова.
Композиция
Отделение начала операции от ее завершения весьма важно для исключения блокирующих вызовов. Проблема в том, что композиция нескольких асинхронных операций, основанных на обратных вызовах, — дело трудное и конечный код сложно читать и отлаживать. Нужно что-то упрощать, чтобы можно было разобраться в получившейся «каше вызовов».
Рассмотрим конкретный пример. Я хочу использовать функцию ReadString из предыдущего примера для последовательного чтения двух файлов и объединять результаты в одну строку. И вновь собираюсь реализовать эту операцию как функцию, принимающую обратный вызов:
template<typename Callback>
void ConcatFiles1(String^ file1, String^ file2, Callback func)
{
ReadString(file1, [func](String^ str1) {
ReadString(file2, [func](String^ str2) {
func(str1+str2);
});
});
}
Неплохо, правда?
Однако, если вы не видите дефект в этом решении, подумайте о следующем: когда вы будете начинать чтение из file2? Действительно ли вам нужно заканчивать чтение первого файла до начала чтения второго? Конечно, нет! Куда лучше запускать несколько асинхронных операций и обрабатывать данные по мере их поступления.
Давайте попробуем. Для начала — из-за запуска двух операций одновременно и возврата из функции до завершения операций — мне потребуется специальный объект, создаваемый в куче, для хранения промежуточных результатов. Назовем его ResultHolder:
ref struct ResultHolder
{
String^ str;
};
Как показано на рис. 2, операция, успешно завершившаяся первой, установит его член results->str. Операция, завершенная второй, будет использовать его для формирования конечного результата.
Рис. 2. Параллельное чтение из двух файлов
template<typename Callback>
void ConcatFiles(String^ file1, String^ file2, Callback func)
{
auto results = ref new ResultHolder();
ReadString(file1, [=](String^ str) {
if(results->str != nullptr) { // остерегайтесь гонок!
func(str + results->str);
}
else{
results->str = str;
}
});
ReadString(file2, [=](String^ str) {
if(results->str != nullptr) { // остерегайтесь гонок!
func(results->str + str);
}
else{
results->str = str;
}
});
}
Это будет работать… по большей части. В этом коде явно создаются условия для гонок, и он не обрабатывает ошибки, поэтому нам еще предстоит пройти длинный путь. Для столь простой задачи вроде объединения двух операций приходится писать до изумления много кода, и не так-то просто заставить его работать правильно.
Задачи в Parallel Patterns Library
Visual Studio Parallel Patterns Library (PPL) предназначена для упрощения и ускорения написания параллельных и асинхронных программ на C++. Вместо того чтобы работать на уровне потоков и их пулов, пользователи PPL имеют дело с более высокоуровневыми абстракциями, такими как задачи (tasks), параллельными алгоритмами вроде parallel_for и parallel_sort, и контейнерами с поддержкой параллельной обработки, например concurrent_vector.
PPL-класс task (новшество в Visual Studio 2012) позволяет вам кратко описать индивидуальную единицу работы, которую нужно выполнять асинхронно. Логику вашей программы можно выразить в терминах независимых (или взаимозависимых) задач и предоставить исполняющей среде позаботиться об оптимальном планировании этих задач.
Самое полезное в задачах — возможность их композиции. В простейшем случае две задачи можно последовательно соединить, объявив одну из них продолжением (continuation) другой. Эта обманчиво тривиальная конструкция позволяет комбинировать несколько задач весьма интересными способами. Многие высокоуровневые PPL-конструкции вроде join и choice (о которых чуть позже) сами построены на основе этой концепции. Продолжения задач также можно использовать для более точного представления завершений асинхронных операций. Давайте пересмотрим пример с рис. 1 и теперь напишем его, используя PPL-задачи, как показано на рис. 3.
Рис. 3. Чтение из файлов с применением вложенных PPL-задач
task<String^> ReadStringTask(String^ fileName)
{
StorageFolder^ item = KnownFolders::PicturesLibrary;
task<StorageFile^> getFileTask(item->GetFileAsync(fileName));
return getFileTask.then([](StorageFile^ storageFile) {
task<IRandomAccessStream^> openTask(storageFile->OpenAsync(
FileAccessMode::Read));
return openTask.then([](IRandomAccessStream^ istream) {
auto reader = ref new DataReader(istream);
task<UINT> loadTask(reader->LoadAsync(istream->Size));
return loadTask.then([reader](UINT bytesRead) {
return reader->ReadString(bytesRead);
});
});
});
}
Поскольку теперь для представления асинхронности я использую задачи вместо обратных вызовов, здесь нет обратного вызова, передаваемого пользователем. Эта инкарнация функции возвращает задачу.
В данной реализации я создал задачу getFileTask из асинхронной операции, возвращаемой GetFileAsync. Затем я определяю завершение этой операции как продолжение задачи (метод then).
Метод then заслуживает более внимательного рассмотрения. Параметр этого метода является лямбда-выражением. На самом деле он мог бы быть и указателем на функцию, и объектом-функцией, и экземпляром std::function, но, поскольку лямбда-выражения вездесущи в PPL (и в современном C++), с этого момента я буду говорить «лямбда», имея в виду любой тип вызываемого объекта.
Метод then возвращает задачу некоего типа T. Этот тип T определяется возвращаемым типом лямбды, переданной в then. В самом базовом виде, когда лямбда возвращает выражение типа T, метод then возвращает task<T>. Например, лямбда в следующем продолжении возвращает int; следовательно, конечным типом будет task<int>:
task<int> myTask = someOtherTask.then([]() { return 42; });
Гораздо лучше запускать несколько асинхронных операций сразу и обрабатывать данные по мере их поступления.
Тип продолжения, используемого на рис. 3, несколько отличается. Он возвращает задачу и выполняет асинхронную развертку (unwrapping) этой задачи, чтобы конечный тип был не task<task<int>>, а task<int>:
task<int> myTask = someOtherTask.then([]() {
task<int> innerTask([]() {
return 42;
});
return innerTask;
});
Если все это пока не совсем понятно, не зацикливайтесь на этом. Обещаю, что после еще нескольких примеров вы точно уловите смысл.
Композиция задач
Вооружившись тем, о чем было рассказано в предыдущем разделе, продолжим обсуждение, опираясь на все тот же пример с чтением файлов.
Вспомните, что в C++ все локальные переменные, находящиеся в функциях и лямбдах, теряются при возврате управления. Чтобы сохранить состояние, вы должны сами копировать переменные в кучу или другое длительно живущее хранилище. Именно по этой причине я ранее создал класс holder. В лямбдах, выполняемых асинхронно, нужно быть осторожным, чтобы не захватить какое-либо состояние из включающей функции по указателю или ссылке; иначе, когда эта функция завершится, вы останетесь с указателем на недействительный участок памяти.
Я воспользуюсь тем фактом, что метод then выполняет развертку в асинхронных интерфейсах, и перепишу наш пример в более четкой форме — несмотря на издержки введения еще одной структуры для хранения (рис. 4).
Рис. 4. Объединение нескольких задач в цепочку
ref struct Holder
{
IDataReader^ Reader;
};
task<String^> ReadStringTask(String^ fileName)
{
StorageFolder^ item = KnownFolders::PicturesLibrary;
auto holder = ref new Holder();
task<StorageFile^> getFileTask(item->GetFileAsync(fileName));
return getFileTask.then([](StorageFile^ storageFile) {
return storageFile->OpenAsync(FileAccessMode::Read);
}).then([holder](IRandomAccessStream^ istream) {
holder->Reader = ref new DataReader(istream);
return holder->Reader->LoadAsync(istream->Size);
}).then([holder](UINT bytesRead) {
return holder->Reader->ReadString(bytesRead);
});
}
В сравнении с примером на рис. 3 этот код легче читать, потому что он подобен последовательным операциям в противоположность «лестнице» вложенных операций.
Кроме метода then, в PPL есть несколько других композиционных конструкций. Одна из них — операция join, реализованная в методе when_all. Метод when_all принимает последовательность задач и возвращает конечную задачу, которая собирает вывод ото всех составляющих ее задач в std::vector. В случае двух аргументов (что встречается довольно часто) PPL предлагает удобное сокращение: оператор &&.
Вот как я использовал оператор join для пересмотренной реализации метода конкатенации файлов:
task<String^> ConcatFiles(String^ file1, String^ file2)
{
auto strings_task = ReadStringTask(file1) &&
ReadStringTask(file2);
return strings_task.then([](std::vector<String^> strings) {
return strings[0] + strings[1];
});
}
Операция choice тоже полезна. При наличии серии задач choice (которая реализована в методе when_any) завершается, когда заканчивается выполнение любой задачи в последовательности. Как и join, у choice есть сокращение для двух аргументов, но в виде оператора ||.
Choice удобна в таких сценариях, как избыточное или спекулятивное выполнение; вы запускаете несколько задач и получаете результат от той, которая завершится первой. Вы также могли бы добавить период ожидания операции — начать с операции, которая возвращает некую задачу, и скомбинировать ее с задачей, которая «засыпает» на заданное время. Если спящая задача завершается первой, период ожидания выполнения вашей операции истек, и ее можно отбросить или отменить.
В PPL имеется еще одна конструкция, помогающая в композиции задач: task_completion_event, которую можно использовать для взаимодействия задач с кодом, где не применяется PPL. Конструкцию task_completion_event можно передавать в поток или обратный вызов завершения ввода-вывода, где ожидается, что это событие в конечном счете будет установлено. Задача, созданная на основе task_completion_event, будет завершена, как только будет установлено task_completion_event.
Создание асинхронных операций с помощью PPL
Всякий раз, когда нужно выжать из компьютера все, на что он способен, в качестве языка программирования следует выбирать C++. Остальные языки занимают в Windows 8 свои ниши: комбинация JavaScript/HTML5 отлично подходит для написания GUI-интерфейсов, C# обеспечивает быструю разработку программ и т. д. Чтобы написать приложение в стиле Metro, используйте тот язык, который вы знаете и который позволяет решать ваши задачи. Тем более что при разработке одного приложения можно задействовать сразу несколько языков программирования.
Зачастую вы предпочтете писать внешний интерфейс приложения на каком-нибудь языке вроде JavaScript или C#, а внутренние компоненты — на C++ для максимальной производительности. Если операция, экспортируемая вашим C++-компонентом, интенсивно нагружает процессор или выполняет большой объем операций ввода-вывода, желательно определить ее как асинхронную.
Для реализации ранее упомянутых асинхронных интерфейсов WinRT (IAsyncOperation, IAsyncAction, IAsyncOperationWithProgress и IAsyncActionWithProgress) в PPL определены метод create_async и класс progress_reporter; оба находятся в пространстве имен concurrency.
PPL предназначена для упрощения и ускорения написания параллельных и асинхронных программ на C++.
В простейшей форме create_async принимает лямбду или указатель на функцию, которая возвращает некое значение. Тип лямбды определяет тип интерфейса, возвращаемого из create_async.
При передаче лямбды без параметров, которая возвращает тип T, отличный от void, метод create_async возвращает реализацию IAsyncOperation<T>. В случае лямбды, возвращающей void, конечным интерфейсом будет IAsyncAction.
Лямбда может принимать параметр типа progress_reporter<P>. Экземпляр этого типа используется для передачи отчетов о прогрессе типа P вызвавшему коду. Например, лямбда, принимающая progress_reporter<int>, может сообщать процент выполненной работы как целочисленное значение. Возвращаемый тип лямбды в данном случае определяет конечный интерфейс — IAsyncOperationWithProgress<T,P> или IAsyncAction<P> (рис. 5).
Рис. 5. Создание асинхронных операций в PPL
IAsyncOperation<float>^ operation = create_async([]() {
return 42.0f;
});
IAsyncAction^ action = create_async([]() {
// Do something, return nothing
});
IAsyncOperationWithProgress<float,int>^
operation_with_progress =
create_async([](progress_reporter<int> reporter) {
for(int percent=0; percent<100; percent++) {
reporter.report(percent);
}
return 42.0f;
});
IAsyncActionWithProgress<int>^ action_with_progress =
create_async([](progress_reporter<int> reporter) {
for(int percent=0; percent<100; percent++) {
reporter.report(percent);
}
});
Чтобы предоставить асинхронную операцию другим WinRT-языкам, определите открытый ref-класс в своем C++-компоненте и предусмотрите функцию, которая возвращает один из четырех асинхронных интерфейсов. Конкретный пример гибридного приложения на C++ и JavaScript вы найдете в PPL Sample Pack (чтобы получить его, выполните поиск в сети по «Asynchrony with PPL»). Вот фрагмент, который предоставляет процедуру преобразования изображения как асинхронную операцию с отчетом о прогрессе:
public ref class ImageTransformer sealed
{
public:
//
// Предоставляет преобразование изображения
// как асинхронную операцию с отчетом о прогрессе
//
IAsyncActionWithProgress<int>^ GetTransformImageAsync(
String^ inFile, String^ outFile);
}
Как показано на рис. 6, клиентская часть приложения реализована на JavaScript.
Рис. 6. Использование процедуры преобразования изображения в JavaScript
var transformer =
new ImageCartoonizerBackend.ImageTransformer();
...
transformer.getTransformImageAsync(copiedFile.path,
dstImgPath).then(
function () {
// Обработка завершения...
},
function (error) {
// Обработка ошибки...
},
function (progressPercent) {
// Обработка прогресса:
UpdateProgress(progressPercent);
}
);
Обработка ошибок и отмена
Внимательные читатели, вероятно, заметили: в этой статье по асинхронности до сих пор напрочь игнорировалось все, что касается обработки ошибок и отмены. Больше этой темой мы пренебрегать не будем!
Рано или поздно процедуре чтения файла будет передана ссылка на файл, которого нет или который нельзя открыть по какой-то причине. Функции поиска по словарю встретится неизвестное ей слово. Преобразование изображения не даст быстрого результата и будет отменено пользователем. В этих ситуациях операция прекращается досрочно, до предполагавшегося момента завершения.
Для указания ошибок или других исключительных ситуаций в современном C++ используются исключения. Они прекрасно работают в рамках одного потока: при генерации исключения происходит раскрутка стека до тех пор, пока по стеку вызовов не будет найдет подходящий блок catch. Однако все это существенно запутывается, когда мы имеем дело с параллельной обработкой, — исключение, исходящее из одного потока, не так-то просто поймать в другом.
Подумайте, что происходит при этом с задачами и продолжениями: когда тело задачи генерирует исключение, его поток выполнения прерывается и он не может дать какое-либо значение. А если нет значения, которое можно было бы передать в продолжение, оно тоже не может работать. Даже для задач, возвращающих void, т. е. не передающих никаких значений, вам нужно возможность сообщить следующей задаче о том, успешно ли завершилась предыдущая.
Вот почему существует альтернативная форма продолжения: для задачи типа T, лямбда продолжения обработки ошибок принимает task<T>. Чтобы получить значение, созданное предыдущей задачей, вы должны вызвать метод get параметра-задачи. Если предшествующая задача завершилась успешно, то же самое будет и с методом get. В ином случае get сгенерирует исключение.
Здесь хотелось бы обратить ваше внимание на важный момент. Для любой задачи в PPL, в том числе созданной из асинхронной операции, синтаксически корректно вызывать ее метод get. Однако, прежде чем результат станет доступен, get пришлось бы блокировать вызвавший поток, и, конечно, это противоречило бы нашей мантре быстроты и гибкости. Поэтому вызывать get из task обычно не рекомендуется, а в STA запрещено (исполняющая среда сгенерирует исключение «недопустимая операция»). Единственный момент, когда вы можете вызвать get, — при получении задачи как параметра продолжения. Пример показан на рис. 7.
Самое полезное в задачах — возможность их композиции.
Рис. 7. Продолжение, обрабатывающее ошибки
cancellation_token_source ct;
task<int> my_task([]() {
// Выполняем какую-то работу.
// Проверяем, не было ли запроса на отмену.
if(is_task_cancellation_requested())
{
// Очищаем ресурсы:
// ...
// Отменяем задачу:
cancel_current_task();
}
// Выполняем еще какую-то работу
return 1;
}, ct.get_token());
...
ct.cancel(); // попытка отмены
Каждое продолжение в вашей программе может быть обрабатывающим ошибки, и вы можете предпочесть обработку исключений в каждом продолжении. Однако в программе, состоящей из множества задач, обработка исключений в каждом продолжении может оказаться сильным перебором. К счастью, этого можно избежать. Подобно необработанным исключениям, проходящим вниз по стеку вызовов до тех пор, пока они не будут захвачены каким-то фреймом, исключения, генерируемые задачами, могут «просачиваться» в следующее продолжение в цепочке до того звена, где они в конце концов будут обработаны. И они должны быть обработаны, потому что, если исключение остается необработанным и по окончании срока жизни задач, которые могли бы его обработать, исполняющая среда генерирует исключение «unobserved exception» («незамеченное исключение»).
Теперь вернемся к нашему примеру с чтением файлов и дополним его обработкой ошибок. Все исключения, генерируемые WinRT, относятся к типу Platform::Exception, поэтому именно такие исключения я буду захватывать в своем последнем продолжении, как показано на рис. 8.
Рис. 8. Чтение строки из файла с обработкой ошибок
task<String^> ReadStringTaskWithErrorHandling(String^ fileName)
{
StorageFolder^ item = KnownFolders::PicturesLibrary;
auto holder = ref new Holder();
task<StorageFile^> getFileTask(item->GetFileAsync(fileName));
return getFileTask.then([](StorageFile^ storageFile) {
return storageFile->OpenAsync(FileAccessMode::Read);
}).then([holder](IRandomAccessStream^ istream) {
holder->Reader = ref new DataReader(istream);
return holder->Reader->LoadAsync(istream->Size);
}).then([holder](task<UINT> bytesReadTask) {
try
{
UINT bytesRead = bytesReadTask.get();
return holder->Reader->ReadString(bytesRead);
}
catch (Exception^ ex)
{
String^ result = ""; // возвращаем пустую строку
return result;
}
});
}
Как только исключение захвачено в каком-либо продолжении, оно считается «обработанным», и это продолжение возвращает задачу, которая успешно завершается. Поэтому на рис. 8 у кода, вызвавшего ReadStringWithErrorHandling, не будет возможности узнать, успешно ли завершилось чтение файла. И здесь я пытаюсь донести до вас тот факт, что преждевременная обработка исключений не всегда является хорошей идеей.
Отмена — это другая форма досрочного завершения задачи. В WinRT, как и в PPL, отмена требует кооперации двух сторон: клиента операции и самой операции. Их роли отличаются: клиент запрашивает отмену, а операция либо принимает этот запрос, либо нет. Из-за естественной гонки между клиентом и операцией нет никаких гарантий, что запрос на отмену окажется удовлетворен.
В PPL эти две роли представлены двумя типами: cancellation_token_source и cancellation_token. Экземпляр первого типа используется для запроса отмены вызовом его метода cancel. Экземпляр второго типа создается из cancellation_token_source и передается как последний параметр в конструктор задачи, метод then или лямбду метода create_async.
Внутри тела задачи эта реализация может проверять запрос отмены, вызывая метод is_task_cancellation_requested, и принимать запрос, вызывая метод cancel_current_task. Поскольку метод cancel_current_task «за кулисами» генерирует исключение, до его вызова следует выполнять очистку некоторых ресурсов. Пример показан на рис. 9.
Рис. 9. Отмена и реакция на запрос отмены в задаче
cancellation_token_source ct;
task<int> my_task([]() {
// Выполняем какую-то работу.
// Проверяем, не было ли запроса на отмену.
if(is_task_cancellation_requested())
{
// Очищаем ресурсы:
// ...
// Отменяем задачу:
cancel_current_task();
}
// Выполняем еще какую-то работу
return 1;
}, ct.get_token());
...
ct.cancel(); // попытка отмены
Заметьте, что многие задачи можно отменить одним и тем же cancellation_token_source. Это очень удобно при работе с цепочками и графами задач. Вместо индивидуальной отмены каждой задачи вы можете отменить все задачи, управляемые данным cancellation_token_source. Конечно, нет никаких гарантий, что какая-нибудь из задач действительно отреагирует на запрос отмены. Такие задачи завершатся, но их обычные продолжения (на основе значений) выполняться не будут. Будут запущены продолжения, обрабатывающие ошибки, но попытка получить значение от предыдущей задачи закончится исключением task_canceled.
Наконец, давайте рассмотрим использование маркеров отмены на «производственной стороне». Лямбда метода create_async может принимать параметр cancellation_token, опрашивать его, используя метод is_canceled, и отменять операцию в ответ на запрос отмены:
IAsyncAction^ action = create_async(
[](cancellation_token ct) {
while (!ct.is_canceled()); // крутимся до отмены
cancel_current_task();
});
...
action->Cancel();
Заметьте, что в случае продолжения задачи маркер отмены принимается методом then, а в случае create_async он передается в лямбду. В последнем варианте отмена инициируется вызовом метода cancel из получаемого асинхронного интерфейса, и это закладывается PPL в запрос отмены через маркер отмены.
Заключение
Как однажды остроумно подметил Тони Хоар (Tony Hoare), нам нужно научить свои программы «ждать быстрее». И все же асинхронное программирование, свободное от ожиданий, остается трудным в освоении, а его преимущества не очевидны на первый взгляд, поэтому разработчики сторонятся его.
В Windows 8 все блокирующие операции являются асинхронными, и, если вы программируете на C++, PPL делает асинхронное программирование вполне доступным. Откройте для себя мир асинхронности и научите свои программы ждать быстрее!