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


Асинхронное программирование в C++/CX

Примечание.

Этот раздел поможет вам поддерживать приложение C++/CX. Но мы рекомендуем использовать C++/WinRT для новых приложений. C++/WinRT — это полностью стандартная проекция языка C++17 для API среды выполнения Windows (WinRT), реализованная как библиотека на основе файлов заголовков и предназначенная для предоставления вам первоклассного доступа к современным интерфейсам API Windows.

В этой статье описывается рекомендуемый способ использования асинхронных методов в расширениях компонентов Visual C++ (C++/CX) с помощью task класса, определенного concurrency в пространстве имен в ppltasks.h.

асинхронные типы среда выполнения Windows

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

Хотя вы можете использовать асинхронные среда выполнения Windows API непосредственно в C++, предпочтительный подход — использовать класс задач и связанные с ним типы и функции, которые содержатся в пространстве имен параллелизма и определены в .<ppltasks.h> Параллелизм::task — это тип общего назначения, но при переключении компилятора /ZW, который необходим для приложений и компонентов универсальная платформа Windows (UWP), используется класс задач инкапсулирует среда выполнения Windows асинхронные типы, чтобы было проще:

  • объединение нескольких асинхронных и синхронных операций

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

  • выполнение отмены в цепочках задач

  • убедитесь, что отдельные задачи выполняются в соответствующем контексте потока или квартире

В этой статье приведены основные рекомендации по использованию класса задач с среда выполнения Windows асинхронными API. Дополнительные сведения о задачах и связанных с ней методах, включая create_task, см. в статье "Параллелизм задач" (среда выполнения параллелизма).

Использование асинхронной операции с помощью задачи

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

  1. create_task Вызовите метод и передайте его объект IAsyncOperation^ .

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

#include <ppltasks.h>
using namespace concurrency;
using namespace Windows::Devices::Enumeration;
...
void App::TestAsync()
{    
    //Call the *Async method that starts the operation.
    IAsyncOperation<DeviceInformationCollection^>^ deviceOp =
        DeviceInformation::FindAllAsync();

    // Explicit construction. (Not recommended)
    // Pass the IAsyncOperation to a task constructor.
    // task<DeviceInformationCollection^> deviceEnumTask(deviceOp);

    // Recommended:
    auto deviceEnumTask = create_task(deviceOp);

    // Call the task's .then member function, and provide
    // the lambda to be invoked when the async operation completes.
    deviceEnumTask.then( [this] (DeviceInformationCollection^ devices )
    {       
        for(int i = 0; i < devices->Size; i++)
        {
            DeviceInformation^ di = devices->GetAt(i);
            // Do something with di...          
        }       
    }); // end lambda
    // Continue doing work or return...
}

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

Метод task::then возвращается немедленно, и его делегат не выполняется, пока асинхронная работа не завершится успешно. В этом примере, если асинхронная операция вызывает исключение или заканчивается в отмененном состоянии в результате запроса на отмену, продолжение никогда не будет выполняться. Позже мы рассмотрим, как писать продолжения, которые выполняются, даже если предыдущая задача была отменена или завершилась сбоем.

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

Создание цепочки задач

В асинхронном программировании обычно определяется последовательность операций, также называемая цепочками задач, в которой каждое продолжение выполняется только при завершении предыдущего. В некоторых случаях предыдущая задача (или предшествующая) создает значение, которое продолжение принимает в качестве входных данных. Используя метод task::then, можно создавать цепочки задач интуитивно понятным и простым способом. Метод возвращает задачу<T, где T> является возвращаемым типом лямбда-функции. Вы можете создать несколько продолжений в цепочку задач: myTask.then(…).then(…).then(…);

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

#include <ppltasks.h>
using namespace concurrency;
...
void App::DeleteWithTasks(String^ fileName)
{    
    using namespace Windows::Storage;
    StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;
    auto getFileTask = create_task(localFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample) ->IAsyncAction^ {       
        return storageFileSample->DeleteAsync();
    }).then([](void) {
        OutputDebugString(L"File deleted.");
    });
}

В предыдущем примере показаны четыре важные моменты:

  • Первое продолжение преобразует объект IAsyncAction^ в пустоту> задачи<и возвращает задачу.

  • Второе продолжение не выполняет обработку ошибок, поэтому принимает пустоту и не является пустой> задачей<в качестве входных данных. Это продолжение на основе значений.

  • Второе продолжение не выполняется до завершения операции DeleteAsync.

  • Так как второе продолжение основано на значении, если операция, запущенная вызовом DeleteAsync , вызывает исключение, второй продолжение не выполняется вообще.

Примечание. Создание цепочки задач — это всего лишь один из способов использования класса задач для создания асинхронных операций. Вы также можете создавать операции с помощью операторов соединения и выбора && и ||. Дополнительные сведения см. в разделе "Параллелизм задач " (среда выполнения параллелизма)".

Лямбда-функция возвращает типы и типы возвращаемых задач

В продолжении задачи возвращаемый тип лямбда-функции упаковывается в объект задачи . Если лямбда-лямбда возвращает двойное значение, то тип задачи продолжения является <двойным>. Однако объект задачи разработан таким образом, чтобы он не создавал не обязательно вложенные типы возвращаемых данных. Если лямбда возвращает IAsyncOperation SyndicationFeed<^^,> продолжение возвращает задачу<SyndicationFeed^, а не< задачу<SyndicationFeed>^>> или задачу<IAsyncOperation<SyndicationFeed^>^^^ .> Этот процесс называется асинхронным распакуванием , а также гарантирует, что асинхронная операция внутри продолжения завершается до вызова следующего продолжения.

В предыдущем примере обратите внимание, что задача возвращает пустоту> задачи<, даже если ее лямбда-объект вернул объект IAsyncInfo. В следующей таблице перечислены преобразования типов, происходящие между лямбда-функцией и заключающей задачей:

лямбда-возвращаемый тип .then тип возвращаемого значения
TResult задача<TResult>
IAsyncOperation<TResult>^ задача<TResult>
IAsyncOperationWithProgress<TResult, TProgress>^ задача<TResult>
IAsyncAction^ задача<void>
IAsyncActionWithProgress TProgress<>^ задача<void>
задача<TResult> задача<TResult>

Отмена задач

Часто рекомендуется предоставить пользователю возможность отменить асинхронную операцию. В некоторых случаях может потребоваться отменить операцию программным способом из-за пределов цепочки задач. Хотя каждый тип возвращаемого значения *Async имеет метод Cancel, который он наследует от IAsyncInfo, он неловко предоставляет его внешним методам. Предпочтительный способ поддержки отмены в цепочке задач — использовать cancellation_token_source для создания cancellation_token, а затем передать маркер конструктору начальной задачи. Если асинхронная задача создается с маркером отмены и вызывается [cancellation_token_source::cancel](/cpp/parallel/concrt/reference/token-token-source-class?view=vs-2017& -view=true), задача автоматически вызывает операцию Cancel on the IAsync* и передает запрос отмены вниз по цепочке продолжения. Следующий псевдокод демонстрирует базовый подход.

//Class member:
cancellation_token_source m_fileTaskTokenSource;

// Cancel button event handler:
m_fileTaskTokenSource.cancel();

// task chain
auto getFileTask2 = create_task(documentsFolder->GetFileAsync(fileName),
                                m_fileTaskTokenSource.get_token());
//getFileTask2.then ...

При отмене задачи task_canceled исключение распространяется по цепочке задач. Продолжения на основе значений просто не будут выполняться, но продолжение на основе задач приведет к возникновению исключения при вызове задачи::get . Если у вас есть продолжение обработки ошибок, убедитесь, что он перехватывает исключение task_canceled явным образом. (Это исключение не является производным от Платформа::Exception.)

Отмена является совместной. Если продолжение выполняет некоторые длительные действия за пределами простого вызова метода UWP, то вы несете ответственность за периодические проверки состояния маркера отмены и остановки выполнения, если оно отменено. После очистки всех ресурсов, выделенных в продолжении, вызовите cancel_current_task , чтобы отменить эту задачу и распространить отмену до любых продолжения на основе значений, которые следуют за ним. Ниже приведен еще один пример: можно создать цепочку задач, представляющую результат операции FileSavePicker. Если пользователь выбирает кнопку "Отмена", метод IAsyncInfo::Cancel не вызывается. Вместо этого операция завершается успешно, но возвращает nullptr. Продолжение может протестировать входной параметр и вызвать cancel_current_task , если входные данные являются nullptr.

Дополнительные сведения см. в разделе "Отмена" в PPL

Обработка ошибок в цепочке задач

Если требуется выполнить продолжение, даже если объект antecedent был отменен или создан исключение, сделайте продолжение продолжением на основе задач путем указания входных данных для его лямбда-функции как задачи TResult> или void задачи<>, если лямбда-задача текedent возвращает IAsyncAction^.<

Для обработки ошибок и отмены в цепочке задач вам не нужно делать каждую операцию на основе задач продолжения или заключать каждую try…catch операцию, которая может вызываться в блоке. Вместо этого можно добавить продолжение на основе задач в конце цепочки и обработать все ошибки. Любое исключение , включающее исключение task_canceled , будет распространяться по цепочке задач и обходить любые продолжения на основе значений, чтобы обрабатывать его в продолжении на основе задач на основе ошибок. Мы можем переписать предыдущий пример, чтобы использовать продолжение на основе задач на основе ошибок:

#include <ppltasks.h>
void App::DeleteWithTasksHandleErrors(String^ fileName)
{    
    using namespace Windows::Storage;
    using namespace concurrency;

    StorageFolder^ documentsFolder = KnownFolders::DocumentsLibrary;
    auto getFileTask = create_task(documentsFolder->GetFileAsync(fileName));

    getFileTask.then([](StorageFile^ storageFileSample)
    {       
        return storageFileSample->DeleteAsync();
    })

    .then([](task<void> t)
    {

        try
        {
            t.get();
            // .get() didn' t throw, so we succeeded.
            OutputDebugString(L"File deleted.");
        }
        catch (Platform::COMException^ e)
        {
            //Example output: The system cannot find the specified file.
            OutputDebugString(e->Message->Data());
        }

    });
}

В продолжении на основе задач мы вызываем задачу-член: :получите результаты задачи. Мы по-прежнему должны вызывать задачу::get, даже если операция была IAsyncAction, которая не приводит к результату, так как task::get также получает все исключения, которые были перенесены в задачу. Если входная задача сохраняет исключение, он создается при вызове задачи::get. Если вы не вызываете задачу::get или не используете продолжение на основе задач в конце цепочки или не перехватывает тип исключения, который был создан, то при удалении всех ссылок на задачу возникает unobserved_task_exception .

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

Управление контекстом потока

Пользовательский интерфейс приложения UWP выполняется в однопоточной квартире (STA). Задача, лямбда-запрос которой возвращает IAsyncAction или IAsyncOperation, учитывается в квартире. Если задача создана в STA, все его продолжения будут выполняться также по умолчанию, если не указано в противном случае. Другими словами, вся цепочка задач наследует сведения об квартире от родительской задачи. Это поведение помогает упростить взаимодействие с элементами управления пользовательского интерфейса, к которым можно получить доступ только из STA.

Например, в приложении UWP в функции-члене любого класса, представляющего страницу XAML, можно заполнить элемент управления ListBox из задачи::затем метод без использования объекта Диспетчера.

#include <ppltasks.h>
void App::SetFeedText()
{    
    using namespace Windows::Web::Syndication;
    using namespace concurrency;
    String^ url = "http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx";
    SyndicationClient^ client = ref new SyndicationClient();
    auto feedOp = client->RetrieveFeedAsync(ref new Uri(url));

    create_task(feedOp).then([this]  (SyndicationFeed^ feed)
    {
        m_TextBlock1->Text = feed->Title->Text;
    });
}

Если задача не возвращает IAsyncAction или IAsyncOperation, то она не учитывается в квартире и, по умолчанию, ее продолжение выполняется в первом доступном фоновом потоке.

Вы можете переопределить контекст потока по умолчанию для любой задачи с помощью перегрузки задачи::тогда она принимает task_continuation_context. Например, в некоторых случаях может потребоваться запланировать продолжение задачи с поддержкой квартиры в фоновом потоке. В таком случае можно передать task_continuation_context::use_arbitrary для планирования работы задачи над следующим доступным потоком в многопотоковой квартире. Это может повысить производительность продолжения, так как его работа не должна быть синхронизирована с другой работой, которая происходит в потоке пользовательского интерфейса.

В следующем примере показано, когда полезно указать параметр task_continuation_context::use_arbitrary , а также показано, как контекст продолжения по умолчанию полезен для синхронизации параллельных операций в непоточных коллекциях. В этом коде мы циклим список URL-адресов для RSS-каналов и для каждого URL-адреса мы запускаем асинхронную операцию для получения данных веб-канала. Мы не можем контролировать порядок получения веб-каналов, и мы не заботимся. После завершения каждой операции RetrieveFeedAsync первый продолжение принимает объект SyndicationFeed^ и использует его для инициализации определяемого приложением FeedData^ объекта. Так как каждая из этих операций не зависит от других, мы можем ускорить работу, указав контекст продолжения task_continuation_context::use_arbitrary . Однако после инициализации каждого FeedData объекта необходимо добавить его в вектор, который не является потокобезопасной коллекцией. Поэтому мы создадим продолжение и указываем [task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017& -view=true), чтобы убедиться, что все вызовы к добавлению происходят в одном контексте однопоточной квартиры приложения (ASTA). Так как task_continuation_context::use_default является контекстом по умолчанию, нам не нужно явно указывать его, но мы делаем это здесь ради ясности.

#include <ppltasks.h>
void App::InitDataSource(Vector<Object^>^ feedList, vector<wstring> urls)
{
                using namespace concurrency;
    SyndicationClient^ client = ref new SyndicationClient();

    std::for_each(std::begin(urls), std::end(urls), [=,this] (std::wstring url)
    {
        // Create the async operation. feedOp is an
        // IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
        // but we don't handle progress in this example.

        auto feedUri = ref new Uri(ref new String(url.c_str()));
        auto feedOp = client->RetrieveFeedAsync(feedUri);

        // Create the task object and pass it the async operation.
        // SyndicationFeed^ is the type of the return value
        // that the feedOp operation will eventually produce.

        // Then, initialize a FeedData object by using the feed info. Each
        // operation is independent and does not have to happen on the
        // UI thread. Therefore, we specify use_arbitrary.
        create_task(feedOp).then([this]  (SyndicationFeed^ feed) -> FeedData^
        {
            return GetFeedData(feed);
        }, task_continuation_context::use_arbitrary())

        // Append the initialized FeedData object to the list
        // that is the data source for the items collection.
        // This all has to happen on the same thread.
        // By using the use_default context, we can append
        // safely to the Vector without taking an explicit lock.
        .then([feedList] (FeedData^ fd)
        {
            feedList->Append(fd);
            OutputDebugString(fd->Title->Data());
        }, task_continuation_context::use_default())

        // The last continuation serves as an error handler. The
        // call to get() will surface any exceptions that were raised
        // at any point in the task chain.
        .then( [this] (task<void> t)
        {
            try
            {
                t.get();
            }
            catch(Platform::InvalidArgumentException^ e)
            {
                //TODO handle error.
                OutputDebugString(e->Message->Data());
            }
        }); //end task chain

    }); //end std::for_each
}

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

Передача обновлений хода выполнения

Методы, поддерживающие IAsyncOperationWithProgress или IAsyncActionWithProgress , периодически предоставляют обновления хода выполнения во время выполнения операции до завершения операции. Отчеты о ходе выполнения не зависят от понятия задач и продолжения. Вы просто предоставляете делегат для свойства Progress объекта. Типичным использованием делегата является обновление индикатора выполнения в пользовательском интерфейсе.