65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 IAsync* WinRT 方法

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2012年6月19日

CPOL

14分钟阅读

viewsIcon

20148

摘自 Windows XAML in Action 的章节

Windows XAML 实战
作者:Pete Brown

基于 Windows Runtime 构建的 Metro 风格应用程序对 UI 线程的任何延迟都特别敏感。用户会立即注意到 UI 滞后于触摸输入,“粘不住手指”。动画卡顿和跳帧也会立即显现。任何处理滞后都会负面影响用户体验,不仅是对你的应用程序,甚至是对整个平台。为了帮助确保你的应用程序不会陷入这种状态,一个好方法就是将耗时长的调用设置为异步。在本文中,基于《Windows 8 XAML in Action》的第三章,作者 Pete Brown 讨论了 Windows Runtime 处理异步代码的方法。

您可能还对以下内容感兴趣…

Windows Runtime 是在异步操作的概念下构建的。它还旨在确保这些异步操作不仅适用于 C#,还适用于 JavaScript、C++ 以及其他许多截然不同的语言。出于这些原因,开发团队没有依赖 .NET 的 Task Parallel Library,而是创建了一种基于接口的方法,与 Windows Runtime 的其他部分保持一致。

Windows.Foundation 命名空间包含了支持 WinRT 中异步功能所需的类型和接口。每个异步操作至少实现 `IAsyncInfo` 接口。像大多数编程问题一样,存在一种简单但有限的异步处理方式,以及一种更复杂但功能更强大的方式。

在本文中,我们将探讨使用 WinRT 异步 API(即返回 IAsync* 接口的那些 API)的不同方法。我将介绍最简单的方法,然后深入探讨检查进度或取消异步操作所需的知识。

Async 和 Await:最简单的方法

最简单、最常用的方法是使用 `await` 关键字。要使用可等待的函数,它必须在一个标记为 `async` 的函数中使用,然后你只需要使用这个关键字,像调用普通同步函数一样调用它,如清单 1 所示。

清单 1 使用 Await 关键字执行网络操作

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  LoadFeedUsingAwait(); #A
}

private async void LoadFeedUsingAwait() #B
{
  var client = new SyndicationClient();

  var uri = new Uri("http://feeds.feedburner.com/PeteBrown");
  var feed = await client.RetrieveFeedAsync(uri);  #C

  Debug.WriteLine(feed.Title.Text);
}
#A Function calls will go here
#B Method marked as async
#C Async call using await

清单 1 中的代码放在新应用程序主页的代码隐藏文件中。注意函数是如何从 `OnNavigatedTo` 中调用的。运行应用程序,然后在(单显示器系统上)切换回 IDE,查看输出窗口。你应该能看到我的博客标题在那里显示。在双显示器系统上,Metro 风格的应用程序会显示在一个显示器上,IDE 显示在另一个显示器上,因此无需切换。

提示

大家都知道,如果是印刷品,它就立即拥有权威的“大写 A”,所以这是我送给大家的一点小礼物。复制这段话并发送给你的经理

亲爱的全球开发者老板们:为了有效进行 x86/x64 硬件上的 Metro 风格开发,需要开发者机器至少配备两个显示器。第二个显示器非常便宜,即使你购买了那些时髦的 3M 多点触控显示器。没有第二个显示器,开发者将在 Metro 和桌面环境之间的上下文切换中浪费宝贵的时间,尤其是在调试会话期间。通过为整个团队配备适当的设备,您可以节省时间、金钱和精力。

将方法标记为 `async` 修饰符会使编译器重构该方法,在每次使用 `await` 运算符的地方插入可能的挂起和恢复点。编译器会为你处理所有的状态管理,所以你不需要真正考虑你的方法可能已经停止运行的事实。为了使用 `await` 运算符,方法必须标记为 `async`。

使用 `await` 运算符可以非常轻松地支持异步调用——支持异步操作所需的所有魔法都封装在你看不到的代码中。这种方法在大多数情况下都有效。但是,当你想要检查操作的进度或取消正在进行的操作时,你必须使用一种稍微复杂的方法。

Async 和 Await 的幕后

当你使用 `async` 和 `await` 时,编译器会为你处理很多事情。任何使用 `async` 修饰符标记的函数都会被重写为状态机。所有局部函数状态都存储在一个结构中。

实现你所编写的实际功能的代码也位于该结构中的一个名为 `MoveNext` 的方法里。局部变量会变成结构中的字段。你的原始函数最终只是一个初始化状态机然后将其控制权传递给它的外壳。

实现代码以一个 `switch` 语句开始,该语句使用当前状态作为输入。根据该状态变量,代码会分支到异步调用之一、检查调用是否已完成并获取结果的代码,或者函数结束。

这个状态机使得函数能够被挂起,并将状态装箱到堆上,以便在异步调用返回后重新激活。

每个完成的步骤都会修改状态。每次调用函数时,开头的 `switch` 语句会分支代码,使其从之前停止的地方继续执行。

最终结果是编译器为你生成了大量的代码。如果你对异步状态机的内部工作原理感到好奇,你可以看到生成的 MSIL(Microsoft Intermediate Language)以及更多内容,请参阅我关于此主题的博客文章:http://bit.ly/asyncstatemachine

长格式异步操作

`async` 和 `await` 关键字是便利功能,但在处理异步操作时并非必需。你仍然可以使用事件处理程序和回调函数的长格式方法。

你可以选择的选项取决于异步函数的返回类型实现的接口。表 1 显示了不同的接口并描述了它们的功能。

接口 描述
IAsyncAction 最基本的异步支持接口。它定义了一个没有返回值的异步方法。
IAsyncActionWithProgress<TProgress> 一个支持报告给定类型进度的接口。
IAsyncOperation<TResult> 一个支持具有返回值的异步方法的接口。
IAsyncOperationWithProgress<TResult,TProgress> 一个支持同时具有返回值和进度报告的异步方法的接口。
表 1 WinRT 中的异步支持接口。所有异步操作都必须返回实现至少一个这些接口的类型。

此外,`IAsyncInfo` 接口通过提供 `Cancel` 和 `Close` 方法,以及获取错误和状态信息来支持异步操作。这四个接口都继承自 `IAsyncInfo`,因此包含了此功能。

你也可以使用长格式方法进行调用。在这种情况下,返回类型实现了功能最丰富的接口:`IAsyncOperationWithProgress`。这意味着在使用 `SyndicationClient` 时,我们可以同时获得结果,并在操作进行过程中获得更新。这是签名

public IAsyncOperationWithProgress<SyndicationFeed,RetrievalProgress>

                  RetrieveFeedAsync(System.Uri uri)

SyndicationFeed 是实际的返回类型——这是你想要的数据。如果此函数在没有考虑异步操作的情况下实现,它可能会看起来像这样

public SyndicationFeed RetrieveFeedAsync(System.Uri uri)

但它是异步的,所以返回类型是一个实现该接口的类。清单 2 展示了如何使用长格式异步模式调用该函数。就像我们在清单 1 中所做的那样,从 `OnNavigatedTo` 中调用此函数。

清单 2 使用长格式方法下载 Syndication Feed

private void LoadFeedUsingLongForm()
{
  var client = new SyndicationClient();
 
  var uri = new Uri("http://feeds.feedburner.com/PeteBrown");
  var op = client.RetrieveFeedAsync(uri);
 
  op.Completed = (info, status) => #A
    {
      switch (status)
      {
        case AsyncStatus.Canceled: #B
          Debug.WriteLine("Operation was canceled");
          break;
        case AsyncStatus.Completed: #C
          Debug.WriteLine("Operation was completed");
          Debug.WriteLine(info.GetResults().Title.Text);
          break;
        case AsyncStatus.Error: #D
          Debug.WriteLine("Operation had an error:");
          Debug.WriteLine(info.ErrorCode.Message);
          break;
      }
    };
}
#A Completed delegate
#B Operation canceled
#C Operation complete
#D Operation error

请注意,Completed 不是一个事件处理程序。相反,它只是一个委托属性。因此,你使用 `=` 而不是 `+=` 来添加你的处理程序代码。如果你不立即注意到,这个细微的差别可能会让你感到困惑。

当你运行这个程序时,`Completed` 委托将被调用,然后它将在 IDE 的调试窗口中输出我的博客标题。和以前一样,你需要从 IDE 中终止应用程序。

关于竞态条件呢?

如果你查看清单 2 中的代码,你可能会想知道执行函数和连接 `Completed` 处理程序之间是否存在潜在的竞态条件。

在遵循事件方法的常规 .NET 异步代码中,你需要在执行操作之前连接事件处理程序。这是为了确保在函数需要事件处理程序之前它们就已经可用。对于可能快速完成的异步操作,这一点尤其重要。

然而,在 Windows Runtime 中,操作在你调用函数后立即开始执行。你无法在声明和执行之间连接处理程序。是否存在这样的竞态条件,即操作在处理程序可用之前就已完成?

幸运的是,Windows Runtime 团队考虑到了这种情况,并确保它不会发生。返回的操作包含了异步函数管理所需的所有内容。如果函数在连接处理程序之前完成,操作仍然存在,并且能够管理上下文和处理程序。在这种情况下,一旦你可用处理程序,操作就会调用它。

所以,你不必担心设置处理程序的竞态条件。团队已经处理过了。

对于像 `ContinueWith` 这样的函数,Task 也考虑到了这一点。

了解任务何时完成至关重要。对于某些耗时任务,你可能希望获取一些关于中间状态的信息。

获取进度更新

一些特别耗时的任务,例如通过慢速连接下载大文件或处理大量数据,应该将进度信息报告回调用代码。这在平板电脑市场尤其重要,因为各地的带宽差异很大。

为了支持灵活的进度报告,`IAsyncActionWithProgress` 和 `IAsyncOperationWithProgress` 接口提供了一种方式,使异步函数能够使用适合该函数的任何数据格式提供状态更新。

在这种情况下,报告类型是 `RetrievalProgress` 结构,它公开两个属性:`bytesRetrieved` 和 `totalBytesToRetrieve`。使用这两个属性,你可以计算完成百分比和剩余百分比。清单 3 展示了如何做到这一点。

清单 3 从 RSS Feed 下载器获取进度报告

private void LoadFeedUsingLongForm()
{
  var client = new SyndicationClient();
 
  var uri = new Uri("http://feeds.feedburner.com/PeteBrown");
  var op = client.RetrieveFeedAsync(uri);
 
  op.Completed = (info, status) =>
    {
      ... #A
    };
 
  op.Progress = (info, progress) => #B
    {
      Debug.WriteLine(string.Format("{0}/{1} bytes {2:P}", 
        progress.bytesRetrieved, 
        progress.totalBytesToRetrieve,
        (float)progress.bytesRetrieved /  
               (float)progress.totalBytesToRetrieve));
    };
}
#A Code in previous listing
#B Progress report

此代码与清单 2 相同,只是增加了 `Progress` 处理程序。在执行期间,按照你正在调用的代码确定的频率,你将通过此处理程序接收进度更新。

然而,当我运行上述代码时,我得到了一些非常有趣的结果。`totalBytesToRetrieve` 不正确,报告了一个无意义的数字。如果我将 URI 改为 http://10rem.net/blog/rss.aspx[1],我得到了正确的结果

4096/205187 bytes 2.00 %
8192/205187 bytes 3.99 %
12288/205187 bytes 5.99 %
16384/205187 bytes 7.98 %
 [snip]
200704/205187 bytes 97.82 %
204800/205187 bytes 99.81 %
205187/205187 bytes 100.00 %
Operation was completed
Pete Brown's Blog (POKE 53280,0)

并非所有服务都能正确报告总字节数。在这种情况下,Feedburner 显然不能。在执行自己的下载时,你需要检查这个数字并相应地调整你的进度报告。一种方法是在未报告总字节数的情况下显示一个不确定的进度条。

那如何处理需要中止的长时间运行的操作呢?你将如何处理这种情况?

取消操作

对于一些特别耗时的操作,例如下载文件,你希望提供一种用户可以取消的方式。过去我不得不这样做时,我会提供一个“取消”按钮,它会设置一个标志。执行长时间运行操作的代码(通常在一个循环或定时器中)会在每次迭代中检查此标志。如果设置了取消标志,操作将终止并进行清理。

在使用 Windows Runtime 中的异步代码时,过程与我描述的方法没有太大区别。但是,你不是设置一个标志,而是调用操作上的 `Cancel` 函数。执行代码使用它来确定需要采取的步骤。

清单 4 是对清单 3 的更新,它将在下载一定数量的字节后取消。我们还没有介绍控件和 UI,所以我暂时不想处理按钮等。

清单 4 下载 5000 字节后取消操作

bool alreadyCancelled = false; #A
op.Progress = (info, progress) =>
  {
    Debug.WriteLine(string.Format("{0}/{1} bytes {2:P}",
        progress.bytesRetrieved,
        progress.totalBytesToRetrieve,
        (float)progress.bytesRetrieved /
              (float)progress.totalBytesToRetrieve));
 
    if (!alreadyCancelled) #A
    {
      if (progress.bytesRetrieved > 5000)
      {
        info.Cancel();        #B
        alreadyCancelled = true;
      }
    }
  };
#A Prevent redundant Cancel calls
#B Cancel operation

此清单展示了一种取消方法。如上所述,通常你会使用一个按钮或其他用户界面设备来调用 `Cancel` 方法。

从这个清单中需要注意的一点是,当你运行它时,你可能不会看到它真正取消,直到操作完成。请记住,这是一个异步操作,函数负责决定何时以及如何快速取消。为了好玩,如果你将 5000 改为 0,你很可能会立即看到它被取消。

查看这些不同的长格式异步代码方法可能会让你反复检查,看你是否在打盹时意外地以 DeLorean 达到了每小时 88 英里。这肯定会让你觉得我们好像倒退了,但随着 `async` 和 `await` 的加入,使得这种冗长的做法成为可选的,我可以向你保证,我们正朝着正确的方向前进。

如果你想要灵活性和强大的功能,请选择长/冗长的版本。如果你不需要那么多灵活性(这可能占大多数情况——打开文件时你真的需要报告进度或提供取消选项吗?),则使用 `async` 和 `await` 关键字可以使你的生活更轻松。不要强迫你自己或你的团队只选择一种方法;根据具体情况使用最适合的方法,并尽可能保持简单。

摘要

在过去的几年里,异步操作已成为应用程序开发中的一个事实。我将此归咎于 Ajax[2] 和 JavaScript,但话说回来,我通常将所有我不喜欢的东西都归咎于这两者。不,真正的原因是用户的期望。曾经有一段时间,在进行长时间计算或其他操作时,应用程序锁定和无响应是完全可以接受的。许多应用程序在执行打印这样简单的事情时都会挂起。

但后来我们开始加入动画,而且不仅仅是 GeoCities 和 Angelfire 上流行的可爱的跳舞宝宝那种动画,而是既有用又能传达信息的动画。导致动画卡顿或跳帧的操作突然成为一个真正的问题。对于浏览器以及 Silverlight 等浏览器插件技术,甚至是 WPF 等桌面技术,推荐的网络(迄今为止最大的延迟问题)方法是使用异步代码。浏览器和 Silverlight 需要它,WPF 则不需要。结果很明显,如果不是必需的,就不会发生。

最后,压垮骆驼的最后一根稻草是触摸交互。触摸 UI 是一种如此直接和深入的交互,以至于任何响应滞后都会立即被注意到。当你在为触摸设计的平板电脑上运行依赖触摸的应用程序时,使用相对低功耗的 ARM 处理器,任何滞后都会立即被注意到。重要的是,看到滞后的用户通常不会想“哇,这个应用程序太烂了”,而是会想“哇,这个平板电脑太烂了”。因此,微软决定将任何稍微慢一点的操作都变成异步 API。

这是一个正确的决定,而且由于 `async` 和 `await` 关键字的加入,这个决定也非常容易接受。除非你想承担额外的复杂性,因为你需要报告进度或提供取消操作的选项,否则你可以简单地使用这些有用的关键字,完全忘记操作是异步的。

Windows Runtime 中到处都包含异步方法。此外,用于编写 C# Metro 风格应用程序的 .NET 库也包含异步代码。但是,这些库使用 Task 类而不是基于 WinRT 接口的异步方法。这有助于使 .NET 与桌面版本以及上一版 .NET 保持一致。值得庆幸的是,你可以像处理 WinRT IAsync* 接口一样轻松地使用 `async` 和 `await` 来处理 .NET Task。此外,如果你真的想保持代码的一致性,你可以使用包含的扩展方法轻松地在两种方法之间进行转换。

以前,异步代码是我们应用程序开发中令人头疼的事情。有了 Task 和 IAsync*,以及新的 `async` 和 `await` 关键字,再加上库中对异步操作的适当使用,我感觉,这是我第一次真正做对了异步。到处都是异步?我对此很满意。

以下是您可能感兴趣的其他 Manning 图书

HTML5 for .NET Developers
Jim Jackson II and Ian Gilman

Learn Windows IIS in a Month of Lunches
Jason Helmick

Silverlight 5 in Action
Pete Brown

[1] 我的源 RSS feed,我用它来 feed 更具可扩展性和可靠的 Feedburner feed

[2] 还有你,jQuery!我知道你在玩什么游戏。

© . All rights reserved.