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

驯服 Silverlight 异步调用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2011年12月4日

CPOL

6分钟阅读

viewsIcon

24849

downloadIcon

305

一个帮助类,用于简化和增强 Silverlight 异步调用

引言

Silverlight 的用户经常会遇到框架强制执行的异步调用模式。其中存在的陷阱包括:

  • 回调方法被调用多次而不是一次。
  • 在信息检索完毕之前就尝试使用这些信息。
  • 无法将特定调用与特定回调关联起来。

关于这个问题,CodeProject 及其他地方已经有很多文章。事实上,语言本身也即将发生一些变化,以便更容易处理一些常见情况。然而,到目前为止,我看到的所有方法都要么要求您重新设计方法,要么强制采用一种“伪同步”方法,在这种方法中,同时进行多个异步调用的优势就丧失了。

AsyncCalls 库解决了这些问题以及更多问题。它提供了:

  • 一种易于阅读的方式来编写简单的“伪同步”方法调用。
  • 能够同时进行对同一方法的多个调用,甚至允许为每个调用运行不同的回调方法,并保证用户代码将为每次异步调用运行且仅运行一次。
  • 能够以这样的方式配置对多个异步方法的多个调用,使得尽可能多的调用能够同时进行,同时保证所有必需的结果都可用。

整个库只有一页长——比下载中包含的演示 Silverlight 应用还要小!

Using the Code

让我们从一个简单的情况开始。我们有一个简单的 Silverlight 应用,带有一个按钮。每次按下按钮时,我们都会访问一个 Web 服务,运行一个可能耗时较长的计算,并在收到结果后更新一个文本字段。

我们第一次尝试的代码可能看起来是这样的(MVVM 的爱好者们请原谅,为了简单起见,这里的一切都将采用代码隐藏风格。即便如此,底层模型在 MVVM 中也更有价值)。

private void button_Click(object sender, RoutedEventArgs e)
{
// assume we have already created a client variable to hold our service reference
// AsyncSvc1Client client = new AsyncSvc1Client(); 
client.slowCompletedEventArgs += (s,e1) =>
 {
 textBox.text = e1.Result;
 }

 client.slowAsync("my parameter");
}

第一次运行这段代码时,它会正常工作。而且很可能还会继续正常工作。然而,每次点击按钮时,事件处理程序都会增加一个新的副本,所以点击 4 次后,它将被调用 4 次。这不是一个好主意,尽管在这种情况下你可能不会注意到。

大多数基础文章都提出了两种解决此问题的方法。

第一种方法是将所有设置移到一个只调用一次的静态位置。当回调不需要访问稍后创建的任何内容时,这种方法效果很好。但在我们的例子中,textBox 只会在我们第一次进入显示的页面时实例化,所以我们必须在页面初始化时添加方法,而页面初始化也可以被调用多次。如果你重新进入页面,你会遇到类似的问题,并需要使用 `static` 方法或变量来做一些棘手的处理。

第二种方法是在异步方法中移除调用处理程序。不幸的是,为了做到这一点,我们不得不放弃内联处理程序的清晰性,创建一个单独的命名方法,我们可以显式地移除它。类似这样:

void myHandler(Object o, slowcall1CompletedEventArgs e) 
{
 client.slowCompleted -= myHandler;
 // Now do something useful
 // .......
}

除了我们因此失去了内联处理程序的清晰性之外,如果你想同时运行同一个方法两次,这种方法仍然不起作用。假设你有两个按钮,每个按钮都更新不同的文本字段,并显示同一个函数不同调用的结果。当第一个调用返回之前,如果两个按钮都被按下,会发生什么……

这时,该介绍我使用我的辅助类来改进上述代码的版本了。它看起来是这样的:

 AsyncRunner<slowcallCompletedEventArgs> act1 = 
        new AsyncRunner<slowcallCompletedEventArgs>();
 act1.invoke = () =>
 {
 client.slowcallAsync(7, 1,act1);
 };
 act1.registerCallback(client, (o, e) =>
 {
 textBox1.Text = e.Result.ToString();
 });
 act1.initiateCall();

我调用的方法 - slowcall - 接受两个参数。第一个参数代表延迟的秒数,这样我们就可以看到竞态条件和等待的效果。第二个参数只是一个数字,调用将其作为结果返回,这样我们就可以证明哪个调用去了哪里。

现在看看代码。注意它很简单。注意我们可以用文本方式在回调之前显示调用。注意我们唯一需要的配置工作是 Async 调用中的最后一个参数,它是辅助类实例的名称。

这就是允许我们跟踪哪个调用去哪里的魔法,它使用了异步服务调用中重要但鲜为人知的可选最终参数——所谓的 UserState,它在调用和回调之间不变地传递。在内部,我们使用 UserState 来忽略我们收到的任何不属于我们特定调用的回调。这就是允许多次同时运行而不相互干扰的技巧。

注意:如果你忘记指定 UserState,你的回调将永远不会被执行。

上面的代码已经非常有用了。它可以安全地放在按钮点击事件或其他被调用多次的方法中,如果你有两个按钮,每个按钮都可以做同样的事情而不会有任何混淆的风险。

现在让我们考虑一个更复杂的情况。假设我们需要进行三个调用。前两个调用都会耗时很长,而第三个调用依赖于前两个结果。我们想要的是能够立即启动前两个调用,但仅在第一个和第二个调用完成后才启动第三个调用。

假设第一个调用需要 5 秒,第二个调用需要 7 秒。如果我们将调用按伪同步方式运行,让每个回调设置下一个调用,那么在调用 3 开始之前需要 12 秒。如果调用 1 和 2 并行运行,那么在调用 3 可以开始之前只需要 7 秒。使用一个额外的辅助类 AsyncCoordinator,我们可以获得我们想要的并行性,如下所示:

 int passedFrom1To3 = -1;
 int passedFrom2To3 = -1;

 AsyncCoordinator coord = new AsyncCoordinator();

 AsyncRunner<slowcallCompletedEventArgs> act1 = 
    new AsyncRunner<slowcallCompletedEventArgs>(coord);
 act1.invoke = () =>
 {
 client.slowcall1Async(7, 1,act1);
 };
 act1.registerCallback(client, (o, e) =>
 {
 if (e.Error != null) return;
 passedFrom1To3 = e.Result;
 textBox1.Text = e.Result.ToString();

 });
 AsyncRunner<slowcallCompletedEventArgs> act2 = 
    new AsyncRunner<slowcallCompletedEventArgs>(coord);
 act2.invoke = () =>
 {
 client.slowcall1Async(3, 2, act2);
 };
 act2.registerCallback(client, (o, e) =>
 {
 if (e.Error != null) return;
 int result = e.Result;
 passedFrom2To3 = e.Result;
 textBox2.Text = result.ToString();
 });
 AsyncRunner<slowcallCompletedEventArgs> act3 = 
    new AsyncRunner<slowcallCompletedEventArgs>(coord);
 act3.invoke = () =>
 {
 // our invoker can refer to the result of call 2 safely
 client.slowcall1Async(1, passedFrom2To3, act3);
 };
 act3.registerCallback(client, (o, e) =>
 {
 if (e.Error != null) return;
 // our callback can also refer to the results of earlier calls safely
 int res = e.Result * 10 + passedFrom1To3;
 textBox3.Text = res.ToString();
 });
 AsyncRunner<slowcallCompletedEventArgs> act4 = 
    new AsyncRunner<slowcallCompletedEventArgs>(coord);
 
// define the mutual dependencies
 act3.dependsOn(act1);
 act3.dependsOn(act2);
 // Finally, kick it all off.
 coord.initiate();

需要注意的关键点是:

  • 我们可以使用普通的局部变量在调用之间通信结果。
  • 我们可以确保有依赖关系的调用不会过早发生,但会尽快发生。
  • 样板代码的总开销非常小。我们编写的几乎所有内容都是应用程序逻辑,并且都包含在单个方法中。

此时,我鼓励您下载并运行附带的示例,其中包含一个稍微复杂一些的示例的完整实现,以及所有重要的类定义,这些类使这一切得以工作。如果您只需要一个解决方案,那么您可能无需再往下看。如果您想了解代码的工作原理,请关注即将发布的文章。

关注点

对我来说,想出这种方法最有趣的地方在于,使它能够正常工作所需的代码量非常少。我怀疑在像 F# 这样的语言中,代码量会更少……

最令人沮丧的方面是需要使用反射,因为这是将事件处理程序有效地传递到辅助类的唯一方法。C# 中的事件处理程序为何以如此尴尬的方式处理,我实在不解。

历史

  • 2011 年 12 月 4 日:首次发布版本
  • 2011 年 12 月 5 日:修复了调用 invoke 方法前缺少的一行,以防止竞态条件,并重新上传了源代码
© . All rights reserved.