用 C++ 为 UWP 应用创建异步操作

本文档介绍当你在通用 Windows 运行时 (UWP) 应用中使用任务类生成基于 Windows 线程池的异步操作时要谨记的一些要点。

异步编程的使用是 Windows 运行时应用模型中的关键组成部分,因为它能使应用保持对用户输入的响应。 可以启动长期运行的任务,而不必阻止 UI 线程,并且可以在以后接收任务的结果。 也可以在任务在后台运行时取消任务和接收进度通知。 文档 C++ 中的异步编程提供 Visual C++ 中可用于创建 UWP 应用的异步模式的概述。 该文档介绍如何使用和创建异步 Windows 运行时操作链。 本节介绍如何使用 ppltasks.h 中的类型生成可供另一个 Windows 运行时组件使用的异步操作,以及如何控制异步工作的执行方式。 还可以考虑阅读 Hilo 中的异步编程模式和提示(使用 C++ 和 XAML 的 Windows 应用商店应用),了解我们如何在 Hilo(一种使用 C++ 和 XAML 的 Windows 运行时应用)中使用任务类实现异步操作。

注意

在 UWP 应用中,你可以使用并行模式库 (PPL)异步代理库。 但是,不能使用任务计划程序或资源管理器。 本文档介绍 PPL 提供的附加功能,这些功能仅适用于 UWP 应用,不适用于桌面应用。

要点

  • 使用 concurrency::create_async 来创建可供其他组件(可能用除 C++ 之外的语言编写)使用的异步操作。

  • 使用 concurrency::progress_reporter 向调用您的异步操作的组件报告进程通知。

  • 使用取消标记实现内部异步操作的取消。

  • create_async 函数的行为取决于传递给它的工作函数的返回类型。 返回任务( task<T>task<void>)的工作函数在调用了 create_async的上下文中同步运行。 返回 Tvoid 的工作函数在任意上下文中运行。

  • 可以使用 concurrency::task::then 方法创建一个可依次运行的任务链。 在 UWP 应用中,任务延续的默认上下文取决于此任务的构造方式。 如果是通过向任务构造函数传递异步操作,或通过传递可返回异步操作的 lambda 表达式创建的任务,那么此任务所有延续的默认上下文是当前上下文。 如果任务不是从异步操作构造的,那么默认情况下,对任务的延续使用任意上下文。 可以用 concurrency::task_continuation_context 类重写默认上下文。

本文档内容

创建异步操作

可以使用并行模式库 (PPL) 中的任务和延续模型定义后台任务以及上一任务完成时要运行的其他任务。 这个功能由 concurrency::task 类提供。 有关此模型和 task 类的详细信息,请参阅 Task Parallelism的上下文中同步运行。

Windows 运行时是可用于创建仅在特殊操作系统环境中运行的 UWP 应用的编程接口。 此类应用使用经授权的函数、数据类型和设备,并且从 Microsoft Store 进行分配。 Windows 运行时由应用程序二进制接口 (ABI) 表示。 ABI 是一种基础二进制协定,该协定允许 Visual C++ 等编程语言使用 Windows 运行时 API。

通过使用 Windows 运行时,可以使用各种编程语言的最佳功能并将它们合并到一个应用中。 例如,可以在 JavaScript 中创建 UI,在 C++ 组件中执行计算密集型应用程序逻辑。 在后台执行这些计算密集型操作的能力是使 UI 保持响应状态的一个关键因素。 因为 task 类特定于 C++,所以必须使用 Windows 运行时接口将异步操作传达给其他组件(可能用除 C++ 之外的语言编写)。 Windows 运行时提供四个接口,可以使用它们来表示异步操作:

Windows::Foundation::IAsyncAction
表示异步操作。

Windows::Foundation::IAsyncActionWithProgress<TProgress>
表示报告进度的异步操作。

Windows::Foundation::IAsyncOperation<TResult>
表示返回结果的异步操作。

Windows::Foundation::IAsyncOperationWithProgress<TResult, TProgress>
表示返回结果并报告进度的异步操作。

动作 的概念是指异步任务不生成值(想一想返回 void的函数)。 操作 的概念是指异步任务确实会生成值。 进程 的概念是指任务可以向调用方报告进程消息。 JavaScript、.NET Framework 和 Visual C++ 均提供自己的方式来创建这些接口的实例,以便跨 ABI 边界使用。 对于 Visual C++,PPL 提供 concurrency::create_async 函数。 此函数会创建表示任务完成的 Windows 运行时异步动作或操作。 create_async 函数采用一个工作函数(通常是 lambda 表达式),以内部方式创建 task 对象,并将此任务包装到四个异步 Windows 运行时接口之一中。

注意

仅在必须创建可从另一语言或另一 Windows 运行时组件访问的功能时才使用 create_async。 当知道操作是由 C++ 代码在同一组件中生成和使用时,可以直接使用 task 类。

create_async 的返回类型由其参数的类型决定。 例如,如果工作函数不返回值并且不报告进度,则 create_async 返回 IAsyncAction。 如果工作函数不返回值,但还会报告进度,则 create_async 返回 IAsyncActionWithProgress。 若要报告进度,请提供 concurrency::progress_reporter 对象作为工作函数的参数。 报告进度的能力使您能够报告已执行的工作量和仍然剩余的工作量(比如以百分比表示)。 还可以使您在结果可用时报告结果。

IAsyncActionIAsyncActionWithProgress<TProgress>IAsyncOperation<TResult>IAsyncActionOperationWithProgress<TProgress, TProgress> 接口均提供可以使您取消异步操作的 Cancel 方法。 task 类与取消标记一起使用。 当使用取消标记来取消工作时,运行时不会启动订阅此标记的新工作。 已处于活动状态的工作会监控其取消标记并在可能时停止。 文档 Cancellation in the PPL中更详细地介绍了这种机制。 可通过两种方式将任务取消与 Windows 运行时 Cancel 方法关联起来。 首先,可以定义传递给 create_async 的工作函数以采用 concurrency::cancellation_token 对象。 当调用 Cancel 方法时,将取消此取消标记,并将常规取消规则应用于支持 create_async 调用的基础 task 对象。 如果没有提供 cancellation_token 对象,则基础 task 对象会隐式定义一个。 在需要以协作方式响应工作函数中的取消时,可定义一个 cancellation_token 对象。 示例:使用 C++ 和 XAML 在 Windows 运行时应用中控制执行部分演示了一个有关如何在通用 Windows 平台 (UWP) 应用(使用自定义的 Windows 运行时 C++ 组件)中使用 C# 和 XAML 执行取消的示例。

警告

在一个任务延续链中,当取消了取消标记时,应始终清理状态,然后调用 concurrency::cancel_current_task。 如果是提早返回而不是调用 cancel_current_task,则操作将转换为已完成状态而非已取消状态。

下表总结了在应用程序中可用于定义异步操作的组合。

创建此 Windows 运行时接口 请从 create_async返回此类型 将这些参数类型传递给您的工作函数以使用隐式取消标记 将这些参数类型传递给您的工作函数以使用显式取消标记
IAsyncAction voidtask<void> (无) (cancellation_token)
IAsyncActionWithProgress<TProgress> voidtask<void> (progress_reporter) (progress_reporter, cancellation_token)
IAsyncOperation<TResult> Ttask<T> (无) (cancellation_token)
IAsyncActionOperationWithProgress<TProgress, TProgress> Ttask<T> (progress_reporter) (progress_reporter, cancellation_token)

可以从传递给 task 函数的工作函数返回一个值或一个 create_async 对象。 这些变体会产生不同的行为。 当返回一个值时,工作函数会包装到 task 中,以使其可在后台线程上运行。 此外,基础 task 使用隐式取消标记。 相反,如果返回 task 对象,则工作函数会同步运行。 因此,如果返回 task 对象,请确保工作函数中的任何较长操作作为任务运行,以使应用程序能够保持响应状态。 此外,基础 task 不使用隐式取消标记。 因此,在从 cancellation_token 返回 task 对象时,如果需要取消支持,则需定义工作函数以采用 create_async对象。

下面的示例演示创建可供另一 Windows 运行时组件使用的 IAsyncAction 对象的各种方式。

// Creates an IAsyncAction object and uses an implicit cancellation token.
auto op1 = create_async([]
{
    // Define work here.
});

// Creates an IAsyncAction object and uses no cancellation token.
auto op2 = create_async([]
{
    return create_task([]
    {
        // Define work here.
    });
});

// Creates an IAsyncAction object and uses an explicit cancellation token.
auto op3 = create_async([](cancellation_token ct)
{
    // Define work here.
});

// Creates an IAsyncAction object that runs another task and also uses an explicit cancellation token.
auto op4 = create_async([](cancellation_token ct)
{
    return create_task([ct]()
    {
        // Define work here.
    });
});

示例: 创建 C++ Windows 运行时组件并从 C# 中使用它

假设有一个使用 XAML 和 C# 的应用,可用它来定义 UI 和 C++ Windows 运行时组件以执行计算密集型操作。 在此示例中,C++ 组件会计算给定范围中的哪些数字是质数。 若要阐释四个 Windows 运行时异步任务接口之间的差异,首先在 Visual Studio 中创建一个“空白解决方案”并将其命名为 Primes。 然后在解决方案中添加一个“Windows 运行时组件” 项目并命名为 PrimesLibrary。 将以下代码添加到生成的 C++ 标头文件中(本示例将 Class1.h 重命名为 Primes.h)。 每个 public 方法定义四个异步接口之一。 返回一个值的方法将返回一个 Windows::Foundation::Collections::IVector<int> 对象。 报告进度的方法生成 double 值,这些值定义了已完成的整体工作的百分比。

#pragma once

namespace PrimesLibrary
{
    public ref class Primes sealed
    {
    public:
        Primes();

        // Computes the numbers that are prime in the provided range and stores them in an internal variable.
        Windows::Foundation::IAsyncAction^ ComputePrimesAsync(int first, int last);

        // Computes the numbers that are prime in the provided range and stores them in an internal variable.
        // This version also reports progress messages.
        Windows::Foundation::IAsyncActionWithProgress<double>^ ComputePrimesWithProgressAsync(int first, int last);

        // Gets the numbers that are prime in the provided range.
        Windows::Foundation::IAsyncOperation<Windows::Foundation::Collections::IVector<int>^>^ GetPrimesAsync(int first, int last);

        // Gets the numbers that are prime in the provided range. This version also reports progress messages.
        Windows::Foundation::IAsyncOperationWithProgress<Windows::Foundation::Collections::IVector<int>^, double>^ GetPrimesWithProgressAsync(int first, int last);
    };
}

注意

按照约定,Windows 运行时中的异步方法名称通常以“Async”结尾。

将以下代码添加到生成的 C++ 标头文件中(本示例将 Class1.cpp 重命名为 Primes.cpp)。 is_prime 函数将确定其输入是否为质数。 剩余方法实现 Primes 类。 对 create_async 的每次调用将使用一个与调用它的方法兼容的签名。 例如,因为 Primes::ComputePrimesAsync 返回 IAsyncAction,提供给 create_async 的工作函数不返回值并且不采用 progress_reporter 对象作为其参数。

// PrimesLibrary.cpp
#include "pch.h"
#include "Primes.h"
#include <atomic>
#include <collection.h>
#include <ppltasks.h>
#include <concurrent_vector.h>

using namespace concurrency;
using namespace std;

using namespace Platform;
using namespace Platform::Collections;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;

using namespace PrimesLibrary;

Primes::Primes()
{
}

// Determines whether the input value is prime. 
bool is_prime(int n)
{
    if (n < 2)
    {
        return false;
    }
    for (int i = 2; i < n; ++i)
    {
        if ((n % i) == 0)
        {
            return false;
        }
    }
    return true;
}

// Adds the numbers that are prime in the provided range  
// to the primes global variable.
IAsyncAction^ Primes::ComputePrimesAsync(int first, int last)
{
    return create_async([this, first, last]
    {
        // Ensure that the input values are in range. 
        if (first < 0 || last < 0)
        {
            throw ref new InvalidArgumentException();
        }
        // Perform the computation in parallel.
        parallel_for(first, last + 1, [this](int n)
        {
            if (is_prime(n))
            {
                // Perhaps store the value somewhere...
            }
        });
    });
}

IAsyncActionWithProgress<double>^ Primes::ComputePrimesWithProgressAsync(int first, int last)
{
    return create_async([first, last](progress_reporter<double> reporter)
    {
        // Ensure that the input values are in range.
        if (first < 0 || last < 0)
        {
            throw ref new InvalidArgumentException();
        }
        // Perform the computation in parallel. 
        atomic<long> operation = 0;
        long range = last - first + 1;
        double lastPercent = 0.0;
        parallel_for(first, last + 1, [&operation, range, &lastPercent, reporter](int n)
        {
            // Report progress message.
            double progress = 100.0 * (++operation) / range;
            if (progress >= lastPercent)
            {
                reporter.report(progress);
                lastPercent += 1.0;
            }

            if (is_prime(n))
            {
                // Perhaps store the value somewhere...
            }
        });
        reporter.report(100.0);
    });
}

IAsyncOperation<IVector<int>^>^ Primes::GetPrimesAsync(int first, int last)
{
    return create_async([this, first, last]() -> IVector<int>^
    {
        // Ensure that the input values are in range. 
        if (first < 0 || last < 0)
        {
            throw ref new InvalidArgumentException();
        }
        // Perform the computation in parallel.
        concurrent_vector<int> primes;
        parallel_for(first, last + 1, [this, &primes](int n)
        {
            // If the value is prime, add it to the global vector.
            if (is_prime(n))
            {
                primes.push_back(n);
            }
        });
        // Sort the results.
        sort(begin(primes), end(primes), less<int>());

        // Copy the results to an IVector object. The IVector 
        // interface makes collections of data available to other 
        // Windows Runtime components.
        auto results = ref new Vector<int>();
        for (int prime : primes)
        {
            results->Append(prime);
        }
        return results;
    });
}

IAsyncOperationWithProgress<IVector<int>^, double>^ Primes::GetPrimesWithProgressAsync(int first, int last)
{
    return create_async([this, first, last](progress_reporter<double> reporter) -> IVector<int>^
    {
        // Ensure that the input values are in range.
        if (first < 0 || last < 0)
        {
            throw ref new InvalidArgumentException();
        }
        // Perform the computation in parallel.
        concurrent_vector<int> primes;
        long operation = 0;
        long range = last - first + 1;
        double lastPercent = 0.0;
        parallel_for(first, last + 1, [&primes, &operation, range, &lastPercent, reporter](int n)
        {
            // Report progress message.
            double progress = 100.0 * (++operation) / range;
            if (progress >= lastPercent)
            {
                reporter.report(progress);
                lastPercent += 1.0;
            }

            // If the value is prime, add it to the local vector. 
            if (is_prime(n))
            {
                primes.push_back(n);
            }
        });
        reporter.report(100.0);

        // Sort the results.
        sort(begin(primes), end(primes), less<int>());

        // Copy the results to an IVector object. The IVector 
        // interface makes collections of data available to other 
        // Windows Runtime components.
        auto results = ref new Vector<int>();
        for (int prime : primes)
        {
            results->Append(prime);
        }
        return results;
    });
}

每个方法会首先执行验证以确保输入参数为非负值。 如果输入值为负,则方法会引发 Platform::InvalidArgumentException。 本节后面部分会对错误处理进行说明。

若要在 UWP 应用中使用这些方法,请使用 Visual C#“空白应用程序(XAML)”模板将另一个项目添加到 Visual Studio 解决方案中。 此示例将项目命名为 Primes。 然后,从 Primes 项目中将一个引用添加到 PrimesLibrary 项目中。

将以下代码添加到 MainPage.xaml。 此代码定义了 UI,因此可以调用 C++ 组件并显示结果。

<Page
    x:Class="Primes.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Primes"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="300"/>
            <ColumnDefinition Width="300"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="125"/>
            <RowDefinition Height="125"/>
            <RowDefinition Height="125"/>
        </Grid.RowDefinitions>

        <StackPanel Grid.Column="0" Grid.Row="0">
            <Button Name="b1" Click="computePrimes">Compute Primes</Button>
            <TextBlock Name="tb1"></TextBlock>
        </StackPanel>

        <StackPanel Grid.Column="1" Grid.Row="0">
            <Button Name="b2" Click="computePrimesWithProgress">Compute Primes with Progress</Button>
            <ProgressBar Name="pb1" HorizontalAlignment="Left" Width="100"></ProgressBar>
            <TextBlock Name="tb2"></TextBlock>
        </StackPanel>

        <StackPanel Grid.Column="0" Grid.Row="1">
            <Button Name="b3" Click="getPrimes">Get Primes</Button>
            <TextBlock Name="tb3"></TextBlock>
        </StackPanel>

        <StackPanel Grid.Column="1" Grid.Row="1">
            <Button Name="b4" Click="getPrimesWithProgress">Get Primes with Progress</Button>
            <ProgressBar Name="pb4"  HorizontalAlignment="Left" Width="100"></ProgressBar>
            <TextBlock Name="tb4"></TextBlock>
        </StackPanel>

        <StackPanel Grid.Column="0" Grid.Row="2">
            <Button Name="b5" Click="getPrimesHandleErrors">Get Primes and Handle Errors</Button>
            <ProgressBar Name="pb5"  HorizontalAlignment="Left" Width="100"></ProgressBar>
            <TextBlock Name="tb5"></TextBlock>
        </StackPanel>

        <StackPanel Grid.Column="1" Grid.Row="2">
            <Button Name="b6" Click="getPrimesCancellation">Get Primes with Cancellation</Button>
            <Button Name="cancelButton" Click="cancelGetPrimes" IsEnabled="false">Cancel</Button>
            <ProgressBar Name="pb6"  HorizontalAlignment="Left" Width="100"></ProgressBar>
            <TextBlock Name="tb6"></TextBlock>
        </StackPanel>
    </Grid>
</Page>

将以下代码添加到 MainPage.xaml 中的 MainPage 类中。 此代码定义了一个 Primes 对象和按钮事件处理程序。

private PrimesLibrary.Primes primesLib = new PrimesLibrary.Primes();

private async void computePrimes(object sender, RoutedEventArgs e)
{
    b1.IsEnabled = false;
    tb1.Text = "Working...";

    var asyncAction = primesLib.ComputePrimesAsync(0, 100000);

    await asyncAction;

    tb1.Text = "Done";
    b1.IsEnabled = true;
}

private async void computePrimesWithProgress(object sender, RoutedEventArgs e)
{
    b2.IsEnabled = false;
    tb2.Text = "Working...";

    var asyncAction = primesLib.ComputePrimesWithProgressAsync(0, 100000);
    asyncAction.Progress = new AsyncActionProgressHandler<double>((action, progress) =>
    {
        pb1.Value = progress;
    });

    await asyncAction;

    tb2.Text = "Done";
    b2.IsEnabled = true;
}

private async void getPrimes(object sender, RoutedEventArgs e)
{
    b3.IsEnabled = false;
    tb3.Text = "Working...";

    var asyncOperation = primesLib.GetPrimesAsync(0, 100000);

    await asyncOperation;

    tb3.Text = "Found " + asyncOperation.GetResults().Count + " primes";
    b3.IsEnabled = true;
}

private async void getPrimesWithProgress(object sender, RoutedEventArgs e)
{
    b4.IsEnabled = false;
    tb4.Text = "Working...";

    var asyncOperation = primesLib.GetPrimesWithProgressAsync(0, 100000);
    asyncOperation.Progress = new AsyncOperationProgressHandler<IList<int>, double>((operation, progress) =>
    {
        pb4.Value = progress;
    });

    await asyncOperation;

    tb4.Text = "Found " + asyncOperation.GetResults().Count + " primes";
    b4.IsEnabled = true;
}

private async void getPrimesHandleErrors(object sender, RoutedEventArgs e)
{
    b5.IsEnabled = false;
    tb5.Text = "Working...";

    var asyncOperation = primesLib.GetPrimesWithProgressAsync(-1000, 100000);
    asyncOperation.Progress = new AsyncOperationProgressHandler<IList<int>, double>((operation, progress) =>
    {
        pb5.Value = progress;
    });

    try
    {
        await asyncOperation;
        tb5.Text = "Found " + asyncOperation.GetResults().Count + " primes";
    }
    catch (ArgumentException ex)
    {
        tb5.Text = "ERROR: " + ex.Message;
    }

    b5.IsEnabled = true;
}

private IAsyncOperationWithProgress<IList<int>, double> asyncCancelableOperation;

private async void getPrimesCancellation(object sender, RoutedEventArgs e)
{
    b6.IsEnabled = false;
    cancelButton.IsEnabled = true;
    tb6.Text = "Working...";

    asyncCancelableOperation = primesLib.GetPrimesWithProgressAsync(0, 200000);
    asyncCancelableOperation.Progress = new AsyncOperationProgressHandler<IList<int>, double>((operation, progress) =>
    {
        pb6.Value = progress;
    });

    try
    {
        await asyncCancelableOperation;
        tb6.Text = "Found " + asyncCancelableOperation.GetResults().Count + " primes";
    }
    catch (System.Threading.Tasks.TaskCanceledException)
    {
        tb6.Text = "Operation canceled";
    }

    b6.IsEnabled = true;
    cancelButton.IsEnabled = false;
}

private void cancelGetPrimes(object sender, RoutedEventArgs e)
{
    cancelButton.IsEnabled = false;
    asyncCancelableOperation.Cancel();
}

这些方法使用 asyncawait 关键字在异步操作完成后更新 UI。 有关 UWP 应用中的异步编码的信息,请参阅线程和异步编程

getPrimesCancellationcancelGetPrimes 方法协同工作以使用户能够取消操作。 当用户选择“取消”按钮时,cancelGetPrimes 方法会调用 IAsyncOperationWithProgress<TResult, TProgress>::Cancel 来取消操作。 管理基础异步操作的并发运行时会引发内部异常类型,该内部异常类型由 Windows 运行时捕捉以传达取消已完成的消息。 有关取消模型的详细信息,请参阅取消

重要

为了使 PPL 能够向 Windows 运行时正确地报告其已取消操作,请不要捕捉此内部异常类型。 这意味着也不应捕捉所有异常 (catch (...))。 如果必须捕捉所有异常,请重新引发异常以确保 Windows 运行时能够完成取消操作。

下图显示了选定每个选项后的 Primes 应用。

Windows Runtime Primes app.

有关使用 create_async 创建可供其他语言使用的异步任务的示例,请参阅在 Bing 地图行程优化器示例中使用 C++

控制执行线程

Windows 运行时使用 COM 线程模型。 在此模型中,根据对象处理其同步的方式,对象被托管在不同的单元中。 线程安全对象托管在多线程单元 (MTA) 中。 必须通过单个线程访问的对象托管在单线程单元 (STA) 中。

在具有 UI 的应用程序中,ASTA(应用程序 STA)线程负责发送窗口消息而且它是进程中唯一可以更新 STA 托管的 UI 控件的线程。 这会产生两种后果。 第一种是,要使应用程序保持响应状态,所有占用大量 CPU 的操作和 I/O 操作都不应在 ASTA 线程上运行。 第二种是,来自后台线程的结果都必须封送回 ASTA 以更新 UI。 在 C++ UWP 应用中,MainPage 和其他 XAML 页面都在 ATSA 中运行。 因此,在 ASTA 中声明的任务延续默认情况下也会在此运行,因此您可以在延续主体中直接更新控件。 但是,如果在另一个任务中嵌套任务,则此嵌套任务中的任何延续都在 MTA 中运行。 因此,您需要考虑是否显式指定这些延续在什么上下文中运行。

从异步操作创建的任务(如 IAsyncOperation<TResult>),使用了特殊语义,可以帮助您忽略线程处理详细信息。 虽然操作可能会在后台线程上运行(或者它可能根本不由线程支持),但其延续在默认情况下一定会在启动了延续操作的单元上运行(换言之,从调用了 task::then的单元运行)。 可以使用 concurrency::task_continuation_context 类来控制延续的执行上下文。 使用这些静态帮助器方法来创建 task_continuation_context 对象:

可以将 task_continuation_context 对象传递给 task::then 方法以显式控制延续的执行上下文,或者可以将任务传递给另一单元,然后调用 task::then 方法以隐式控制执行上下文。

重要

由于 UWP 应用的主 UI 线程在 STA 下运行,因此在该 STA 中创建的延续默认情况下在 STA 中运行。 相应地,在 MTA 中创建的延续将在 MTA 中运行。

下面一节介绍一种应用程序,该应用程序从磁盘读取一个文件,查找该文件中最常见的单词,然后在 UI 中显示结果。 最终操作(更新 UI)将在 UI 线程上发生。

重要

此行为特定于 UWP 应用。 对于桌面应用程序,您无法控制延续的运行位置。 相反,计划程序会选择要运行每个延续的辅助线程。

重要

对于在 STA 中运行的延续的主体,请不要调用 concurrency::task::wait 。 否则,运行时会引发 concurrency::invalid_operation ,原因是此方法阻止当前线程并可能导致应用停止响应。 但是,你可以调用 concurrency::task::get 方法来接收基于任务的延续中的先行任务的结果。

示例:使用 C++ 和 XAML 在 Windows 运行时应用中控制执行

假设有一个 C++ XAML 应用程序,该应用程序从磁盘读取一个文件,在该文件中查找最常见的单词,然后在 UI 中显示结果。 若要创建此应用,请首先在 Visual Studio 中创建“空白应用(通用 Windows)”项目并将其命名为 CommonWords。 在应用程序清单中,指定“文档库” 功能以使应用程序能够访问“文档”文件夹。 同时将文本 (.txt) 文件类型添加到应用程序清单的声明部分。 有关应用功能和声明的详细信息,请参阅 Windows 应用的打包、部署和查询

更新 MainPage.xaml 中的 Grid 元素,以包含 ProgressRing 元素和 TextBlock 元素。 ProgressRing 指示操作正在进行, TextBlock 显示计算的结果。

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <ProgressRing x:Name="Progress"/>
    <TextBlock x:Name="Results" FontSize="16"/>
</Grid>

将以下 #include 语句添加到 pch.h

#include <sstream>
#include <ppltasks.h>
#include <concurrent_unordered_map.h>

将以下方法声明添加到 MainPage 类 (MainPage.h)。

private:
    // Splits the provided text string into individual words.
    concurrency::task<std::vector<std::wstring>> MakeWordList(Platform::String^ text);

    // Finds the most common words that are at least the provided minimum length.
    concurrency::task<std::vector<std::pair<std::wstring, size_t>>> FindCommonWords(const std::vector<std::wstring>& words, size_t min_length, size_t count);

    // Shows the most common words on the UI.
    void ShowResults(const std::vector<std::pair<std::wstring, size_t>>& commonWords);

将以下 using 语句添加到 MainPage.cpp。

using namespace concurrency;
using namespace std;
using namespace Windows::Storage;
using namespace Windows::Storage::Streams;

在 MainPage.cpp 中,实现 MainPage::MakeWordListMainPage::FindCommonWordsMainPage::ShowResults 方法。 MainPage::MakeWordListMainPage::FindCommonWords 执行计算密集型操作。 MainPage::ShowResults 方法在 UI 中显示计算的结果。

// Splits the provided text string into individual words.
task<vector<wstring>> MainPage::MakeWordList(String^ text)
{
    return create_task([text]() -> vector<wstring>
    {
        vector<wstring> words;

        // Add continuous sequences of alphanumeric characters to the string vector.
        wstring current_word;
        for (wchar_t ch : text)
        {
            if (!iswalnum(ch))
            {
                if (current_word.length() > 0)
                {
                    words.push_back(current_word);
                    current_word.clear();
                }
            }
            else
            {
                current_word += ch;
            }
        }

        return words;
    });
}

// Finds the most common words that are at least the provided minimum length.
task<vector<pair<wstring, size_t>>> MainPage::FindCommonWords(const vector<wstring>& words, size_t min_length, size_t count)
{
    return create_task([words, min_length, count]() -> vector<pair<wstring, size_t>>
    {
        typedef pair<wstring, size_t> pair;

        // Counts the occurrences of each word.
        concurrent_unordered_map<wstring, size_t> counts;

        parallel_for_each(begin(words), end(words), [&counts, min_length](const wstring& word)
        {
            // Increment the count of words that are at least the minimum length. 
            if (word.length() >= min_length)
            {
                // Increment the count.
                InterlockedIncrement(&counts[word]);
            }
        });

        // Copy the contents of the map to a vector and sort the vector by the number of occurrences of each word.
        vector<pair> wordvector;
        copy(begin(counts), end(counts), back_inserter(wordvector));

        sort(begin(wordvector), end(wordvector), [](const pair& x, const pair& y)
        {
            return x.second > y.second;
        });

        size_t size = min(wordvector.size(), count);
        wordvector.erase(begin(wordvector) + size, end(wordvector));

        return wordvector;
    });
}

// Shows the most common words on the UI. 
void MainPage::ShowResults(const vector<pair<wstring, size_t>>& commonWords)
{
    wstringstream ss;
    ss << "The most common words that have five or more letters are:";
    for (auto commonWord : commonWords)
    {
        ss << endl << commonWord.first << L" (" << commonWord.second << L')';
    }

    // Update the UI.
    Results->Text = ref new String(ss.str().c_str());
}

修改 MainPage 构造函数,以创建一个在 UI 中显示荷马的 伊利亚特 一书中常见单词的延续任务链。 前两个延续任务会将文本拆分为单个词并查找常见词,这会非常耗时,因此将其显式设置为在后台运行。 最终延续任务(即更新 UI)不指定延续上下文,因此遵循单元线程处理规则。

MainPage::MainPage()
{
    InitializeComponent();

    // To run this example, save the contents of http://www.gutenberg.org/files/6130/6130-0.txt to your Documents folder.
    // Name the file "The Iliad.txt" and save it under UTF-8 encoding.

    // Enable the progress ring.
    Progress->IsActive = true;

    // Find the most common words in the book "The Iliad".

    // Get the file.
    create_task(KnownFolders::DocumentsLibrary->GetFileAsync("The Iliad.txt")).then([](StorageFile^ file)
    {
        // Read the file text.
        return FileIO::ReadTextAsync(file, UnicodeEncoding::Utf8);

        // By default, all continuations from a Windows Runtime async operation run on the 
        // thread that calls task.then. Specify use_arbitrary to run this continuation 
        // on a background thread.
    }, task_continuation_context::use_arbitrary()).then([this](String^ file)
    {
        // Create a word list from the text.
        return MakeWordList(file);

        // By default, all continuations from a Windows Runtime async operation run on the 
        // thread that calls task.then. Specify use_arbitrary to run this continuation 
        // on a background thread.
    }, task_continuation_context::use_arbitrary()).then([this](vector<wstring> words)
    {
        // Find the most common words.
        return FindCommonWords(words, 5, 9);

        // By default, all continuations from a Windows Runtime async operation run on the 
        // thread that calls task.then. Specify use_arbitrary to run this continuation 
        // on a background thread.
    }, task_continuation_context::use_arbitrary()).then([this](vector<pair<wstring, size_t>> commonWords)
    {
        // Stop the progress ring.
        Progress->IsActive = false;

        // Show the results.
        ShowResults(commonWords);

        // We don't specify a continuation context here because we want the continuation 
        // to run on the STA thread.
    });
}

注意

此示例演示了如何指定执行上下文以及如何构成延续链。 回想一下,从异步操作创建的任务默认情况下在调用了 task::then的单元上运行其延续。 因此,此示例使用 task_continuation_context::use_arbitrary 来指定不涉及 UI 的操作在后台线程上执行。

下图显示 CommonWords 应用的结果。

Windows Runtime CommonWords app.

在此示例中,可以支持取消操作,因为支持 create_asynctask 对象使用了隐式取消标记。 如果您的任务需要以协作方式响应取消,则请定义您的工作函数以采用 cancellation_token 对象。 有关 PPL 中取消操作的详细信息,请参阅 Cancellation in the PPL

另请参阅

并发运行时