使用 C++/CX 进行异步编程

注意

本主题旨在帮助你维护 C++/CX 应用程序。 不过,我们建议你使用 C++/WinRT 编写新应用程序。 C++/WinRT 是 Windows 运行时 (WinRT) API 的完全标准新式 C++17 语言投影,以基于标头文件的库的形式实现,旨在为你提供对新式 Windows API 的一流访问。

本文介绍在 Visual C++ 组件扩展 (C++/CX) 中,通过使用在 ppltasks.h 中的 concurrency 命名空间中定义的 task 类来使用异步方法的推荐方法。

Windows 运行时异步类型

Windows 运行时功能具有一个定义完善的用于调用异步方法的模型,并提供了使用此类方法所需的类型。 如果你对 Windows 运行时异步模型不熟悉,请在阅读本文其余部分之前阅读异步编程

尽管可通过 C++ 直接使用异步 Windows 运行时 API,但首选方法是使用任务类及其相关类型和函数,它们都包含在并发命名空间中且在 <ppltasks.h> 中定义。 concurrency::task 是一个通用类型,但当使用 /ZW 编译器开关(对于通用 Windows 平台 (UWP) 应用和组件而言必不可少)时,task 类将封装 Windows 运行时异步类型,以便简化以下操作:

  • 将多个异步和同步操作链接在一起

  • 处理任务链中的异常

  • 在任务链中执行取消

  • 确保单个任务在适当的线程上下文或单元中运行

本文提供有关如何将 task 类与 Windows 运行时异步 API 一起使用的基本指南。 有关“任务”及其相关方法(包括 create_task)的更完整的文档,请参阅任务并行(并发运行时)

通过任务使用异步操作

以下示例演示如何通过任务类来使用返回 IAsyncOperation 接口及其操作生成值的“异步”方法。 下面是基本步骤:

  1. 调用 create_task 方法并向其传递 IAsyncOperation^ 对象。

  2. 在任务上调用成员函数 task::then,并提供将在异步操作完成时调用的 lambda。

#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...
}

task::then 函数创建和返回的任务称为“延续”。 用户提供的 lambda 的输入参数(在本例中),是任务操作在完成时生成的结果。 如果直接使用 IAsyncOperation 接口,该值是调用 IAsyncOperation::GetResults 检索的相同值。

task::then 方法会立即返回,在异步工作成功完成之前,其委托不会运行。 在此示例中,如果异步操作导致异常,或者由于取消请求而以取消状态结束,则延续永远不会执行。 稍后,我们将介绍如何编写在前一个任务被取消或失败时仍会执行的延续。

尽管在本地堆栈上声明任务变量,但它管理其生存期,以便直到所有操作完成并且对其的所有引用超出范围时才会删除,即使该方法在操作完成之前返回也是如此。

创建任务链

在异步编程中,通常定义一系列操作,也称为“任务链”,其中每个延续仅在上一个完成时才执行。 在某些情况下,上一个(或“前一个”)任务生成延续会将之接受为输入的值。 通过使用 task::then 方法,可以按照直观且简单的方式创建任务链;该方法返回一个 task<T>,其中 T 是 lambda 函数的返回类型。 你可以将多个延续组合到一个任务链中: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^ 对象转换为 task<void> 并返回 task

  • 第二个延续执行无错误处理,因此接受 void 而不是 task<void> 作为输入。 它是基于值的延续。

  • DeleteAsync 操作完成之前,第二个延续不会执行。

  • 由于第二个延续是基于值的,因此,如果调用 DeleteAsync 启动的操作引发异常,则第二个延续不会执行。

注意 创建任务链只是使用“任务”类撰写异步操作的方法之一。 还可以使用拼接和选择运算符 &&|| 编写操作。 有关详细信息,请参阅任务并行(并发运行时)

Lambda 函数返回类型和任务返回类型

在任务延续中,lambda 函数的返回类型整合在“任务”对象中。 如果该 lambda 返回 double,则延续任务的类型为 task<double>。 但是,任务对象的设计目的是不生成不必要的嵌套返回类型。 如果 lambda 返回 IAsyncOperation<SyndicationFeed^>^,则延续返回 task<SyndicationFeed^>,而不是 task<task<SyndicationFeed^>> 或 task<IAsyncOperation<SyndicationFeed^>^>^。 此过程称为“异步解包”,它还可确保延续中的异步操作在调用下一个延续之前完成。

在前面的示例中,请注意,即使任务 lambda 返回了 IAsyncInfo 对象,任务也会返回一个任务<void> 下表汇总了 lambda 函数与封闭任务之间发生的类型转换:

lambda 返回类型 .then 返回类型
TResult task<TResult>
IAsyncOperation<TResult>^ task<TResult>
IAsyncOperationWithProgress<TResult, TProgress>^ task<TResult>
IAsyncAction^ task<void>
IAsyncActionWithProgress<TProgress>^ task<void>
task<TResult> task<TResult>

取消任务

为用户提供取消异步操作的选项通常是一个好主意。 在某些情况下,可能必须从任务链外部以编程方式取消操作。 尽管每个 *Async 返回类型都有一个从 IAsyncInfo 继承的 Cancel 方法,但将其公开给外部方法不可取。 在任务链中支持取消的首选方式是使用 cancellation_token_source 创建 cancellation_token,然后将该令牌传递给初始任务的构造函数。 如果使用取消令牌创建异步任务,并且调用 [cancellation_token_source::cancel](/cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017& -view=true),则该任务会自动对 IAsync* 操作调用 Cancel,并且将取消请求沿着其延续链向下传递。 以下伪代码演示了基本方法。

//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 异常将沿着任务链向下传播。 基于值的延续将不执行,但基于任务的延续将导致在调用 task::get 时引发异常。 如果存在错误处理延续,请确保它显式捕获 task_canceled 异常。 (此异常非派生自 Platform::Exception。)

取消需要彼此协作。 如果你的延续执行了一些长时间运行的工作,而不仅仅是调用 UWP 方法,则你有责任定期检查取消令牌的状态,并在取消时停止执行。 在清理延续中分配的所有资源之后,请调用 cancel_current_task 取消该任务并将取消向下传播到后跟的任何基于值的延续。 下面是另一个示例:可以创建表示 FileSavePicker 操作结果的任务链。 如果用户选择“取消”按钮,则不会调用 IAsyncInfo::Cancel 方法。 相反,该操作成功,但返回 nullptr。 延续可以测试输入参数,如果输入是 nullptr,则调用 cancel_current_task

有关详细信息,请参阅 PPL 中的取消操作

处理任务链中的错误

如果要让一个延续即使在先行被取消或引发异常的情况下也能够执行,请将该延续的 lambda 函数的输入指定为 task<TResult>task<void>,使该延续成为基于任务的延续,但前提是先行任务的 lambda 返回 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());
        }

    });
}

在基于任务的延续中,调用成员函数 task::get 以获取任务的结果。 我们仍必须调用 task::get,即使操作是未生成任何结果的 IAsyncAction,因为 task::get 还会获取已传输到任务的任何异常。 如果输入任务存储异常,则会在调用 task::get 时引发该异常。 如果不调用 task::get,或者不在任务链的末尾使用基于任务的延续,或者不捕获所引发的异常类型,则当删除对该任务的所有引用时,会引发 unobserved_task_exception

仅捕获可以处理的异常。 如果应用遇到无法从中恢复的错误,最好让应用崩溃,而不是让它继续以未知状态运行。 另外,一般情况下,不要尝试捕获 unobserved_task_exception 本身。 此异常主要用于诊断目的。 当引发 unobserved_task_exception 时,通常表示代码中存在 Bug。 原因通常是应处理的异常,或代码中其他错误导致的不可恢复异常。

管理线程上下文

UWP 应用的 UI 在单线程单元 (STA) 中运行。 lambda 返回 IAsyncActionIAsyncOperation 的任务可识别单元。 如果任务是在 STA 中创建的,则其所有延续将在默认情况下于其中运行,除非另行指定。 换句话说,整个任务链从父任务继承单元感知。 此行为有助于简化与 UI 控件的交互,这些控件只能从 STA 访问。

例如,在 UWP 应用中,在任何表示 XAML 页面的类的成员函数中,可以从 task::then 方法中填充 ListBox 控件,而无需使用 Dispatcher 对象。

#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;
    });
}

如果任务未返回 IAsyncActionIAsyncOperation,则它不会识别单元,并且默认情况下,其延续在第一个可用的后台线程上运行。

你可以使用 task::then 的接受 task_continuation_context 的重载来替代任一种任务的默认线程上下文。 例如,在某些情况下,可能最好在后台线程上计划单元感知任务的延续。 在此情况下,你可以传递 task_continuation_context::use_arbitrary,以便在多线程单元中的下一个可用线程上计划该任务的工作。 这可以提高持续运行的性能,因为其工作不必与 UI 线程上的其他工作同步。

以下示例演示何时指定 task_continuation_context::use_arbitrary 选项有用,还说明了默认延续上下文在同步非线程安全集合上的并发操作方面的作用。 在此代码中,我们循环访问 RSS 提要的 URL 列表,对于每个 URL,我们将启动异步操作来检索提要数据。 我们无法控制检索提要的顺序,我们也并不在乎。 每个 RetrieveFeedAsync 操作完成后,第一个延续接受 SyndicationFeed^ 对象,并使用它初始化应用定义的 FeedData^ 对象。 因为上述每个操作都独立于其他操作,所以我们可以通过指定 task_continuation_context::use_arbitrary 延续上下文来提高运行速度。 但是,在初始化每个 FeedData 对象后,我们必须将其添加到 Vector,而不是线程安全的集合。 因此,我们将创建延续并指定 [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
}

嵌套任务是持续运行内创建的新任务,不会继承初始任务的单元感知。

处理进度更新

支持 IAsyncOperationWithProgressIAsyncActionWithProgress 的方法在操作完成之前定期提供进度更新。 进度报告独立于任务和延续的概念。 只需为对象的进度属性提供委托。 委托的典型用途是在 UI 中更新进度栏。