在现代软件开发中,异步编程模式是处理并发任务和提高应用程序性能的关键技术之一。然而,异步编程也带来了一些挑战,如回调方法被多次调用、在信息被检索之前就尝试使用它、以及无法将特定调用与特定回调关联等问题。这些问题不仅增加了代码的复杂性,还可能导致应用程序出现难以预测的行为。本文将介绍一个名为AsyncCalls的库,它旨在解决这些问题,并提供一种更简单、更直观的方式来处理异步调用。
异步编程的一个常见问题是回调方法可能会被多次调用,而不是只调用一次。这通常是因为每次触发异步操作时,都会为同一个事件添加一个新的事件处理程序,导致当事件被触发时,所有的处理程序都会被调用。此外,如果尝试在异步操作完成之前使用信息,可能会导致应用程序崩溃或产生不可预测的结果。最后,由于异步调用的非阻塞特性,很难将特定的调用与特定的回调关联起来,这使得调试和维护变得更加困难。
AsyncCalls库提供了一种简单的方式来编写看似同步的异步方法调用,同时允许同时进行多个相同方法的调用,并且可以为每个调用指定不同的回调方法。此外,它还保证了用户代码对于每个异步调用只会运行一次。更重要的是,这个库非常轻量级,整个库的代码只有一页长,甚至比下载的演示Silverlight应用程序还要小。
让从一个简单的场景开始。假设有一个Silverlight应用程序,其中包含一个按钮。每次点击按钮时,都会调用一个Web服务,运行一个可能需要很长时间的计算,然后在得到结果后更新一个文本字段。
private void button_Click(object sender, RoutedEventArgs e)
{
AsyncSvc1Client client = new AsyncSvc1Client();
client.slowCompleted += (s, e1) =>
{
textBox.Text = e1.Result.ToString();
};
client.slowAsync("my parameter");
}
第一次运行这段代码时,它会正常工作。但是,每次点击按钮时,都会为事件添加一个新的处理程序,所以点击四次后,它会被调用四次。这显然不是一个好主意,尽管在这种情况下可能不会注意到。
为了解决这个问题,通常有两种方法。第一种方法是将所有设置移到一个只调用一次的静态位置。这种方法在回调不需要访问稍后创建的内容时效果很好。然而,在案例中,textBox只有在首次进入显示页面时才会被实例化,所以需要在页面初始化器中添加方法,如果重新进入页面,也会遇到类似的问题,需要使用静态方法或变量来解决。
第二种方法是在异步方法中移除调用处理程序。不幸的是,为了做到这一点,必须放弃内联处理程序的清晰度,并创建一个可以显式移除的单独命名方法。
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来忽略没有收到的任何回调,这是允许多个运行同时进行而不相互干扰的技巧。
假设有三个调用要做。第一个和第二个每个都需要很长时间,而第三个依赖于前两个结果。想要的是立即启动前两个调用,但只有在前两个完成后才启动第三个调用。
假设第一个调用需要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();
关键要注意的是:
在这一点上,鼓励下载并运行附带的示例,它包含了一个稍微复杂的例子的完整实现,以及使它全部工作的类的定义。如果正在寻找一个解决方案,可能不需要再进一步。如果想了解代码是如何工作的,请期待即将发表的文章。