异步编程模式与解决方案

在现代软件开发中,异步编程模式是处理并发任务和提高应用程序性能的关键技术之一。然而,异步编程也带来了一些挑战,如回调方法被多次调用、在信息被检索之前就尝试使用它、以及无法将特定调用与特定回调关联等问题。这些问题不仅增加了代码的复杂性,还可能导致应用程序出现难以预测的行为。本文将介绍一个名为AsyncCalls的库,它旨在解决这些问题,并提供一种更简单、更直观的方式来处理异步调用。

异步编程的挑战

异步编程的一个常见问题是回调方法可能会被多次调用,而不是只调用一次。这通常是因为每次触发异步操作时,都会为同一个事件添加一个新的事件处理程序,导致当事件被触发时,所有的处理程序都会被调用。此外,如果尝试在异步操作完成之前使用信息,可能会导致应用程序崩溃或产生不可预测的结果。最后,由于异步调用的非阻塞特性,很难将特定的调用与特定的回调关联起来,这使得调试和维护变得更加困难。

AsyncCalls库简介

AsyncCalls库提供了一种简单的方式来编写看似同步的异步方法调用,同时允许同时进行多个相同方法的调用,并且可以为每个调用指定不同的回调方法。此外,它还保证了用户代码对于每个异步调用只会运行一次。更重要的是,这个库非常轻量级,整个库的代码只有一页长,甚至比下载的演示Silverlight应用程序还要小。

使用AsyncCalls库

让从一个简单的场景开始。假设有一个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类

在这一点上,是时候介绍版本代码了,它使用了帮助类。它看起来像这样:

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();

关键要注意的是:

  • 可以使用普通局部变量在调用之间通信结果。
  • 可以确保有依赖关系的调用不会太早发生,但会尽快发生。
  • 在样板代码方面的总开销是最小的。写的几乎所有东西都是应用程序逻辑,而且都在一个方法内。

在这一点上,鼓励下载并运行附带的示例,它包含了一个稍微复杂的例子的完整实现,以及使它全部工作的类的定义。如果正在寻找一个解决方案,可能不需要再进一步。如果想了解代码是如何工作的,请期待即将发表的文章。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485