驯服 Silverlight 异步调用





5.00/5 (2投票s)
一个帮助类,用于简化和增强 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
方法前缺少的一行,以防止竞态条件,并重新上传了源代码