通过 C# 简化 APM


  曾一直赞扬异步编程模型 (APM) 的优点,强调异步执行 I/O 密集型操作是生产高响应和可伸缩应用程序及组件的关键。这些目标是可以达成的,因为 APM 可让您使用极少量的线程来执行大量的工作,而无需阻止任何线程。遗憾的是,使用 APM 构建典型的应用程序或组件有些难度,因此许多程序员不愿意去做。

  有几个因素使得 APM 难以实现。首先,您需要避免状态数据驻留于线程的堆栈上,因为线程堆栈上的数据不能迁移到其他线程。避免基于堆栈的数据意味着必须避免方法参数和局部变量。多年以来,开发人员已喜欢上参数和局部变量,因为它们使编程变得简单多了。

  其次,您需要将代码拆分为几个回调方法,有时称为“续”。例如,如果在一个方法中开始异步读取或写入操作,之后必须实现要调用的另一个方法(可能通过不同的线程),以处理 I/O 操作的结果。但程序员就是不习惯考虑以这种方法进行数据处理。在方法和线程之间迁移状态意味着状态必须进行打包,导致实现过程复杂化。

  再次,众多有用的编程构造 — 如 try/catch/finally 语句、lock 语句、using 语句,甚至是循环(for、while 和 foreach 语句)— 不能在多个方法之间进行拆分。避免这些构造也增加了实现过程的复杂性。

  最后,尝试提供多种功能,如协调多个重叠操作的结果、取消、超时,以及将 UI 控件修改封送到 Windows® 窗体或 Windows Presentation Foundation (WPF) 应用程序中的 GUI 线程,这都为使用 APM 增加了更多的复杂性。

  在本期的专栏中,我将演示 C# 编程语言的一些最新添加内容,它们大大简化了异步编程模型的使用。之后我会介绍我自己的一个类,称为 AsyncEnumerator,它建立在这些 C# 语言功能的基础上,用来解决我刚提到的问题。我的 AsyncEnumerator 类能够让您在代码中使用 APM 变得简单而有趣。通过此类,您的代码会变得可伸缩且高响应,因此没有理由不使用异步编程。请注意,AsyncEnumerator 类是 Power Threading 库的一部分,并且依赖于同样是此库一部分的其他代码;读者可从 Wintellect.com 下载该库。

  匿名方法和 lambda 表达式

  SynchronousPattern 方法显示了如何同步打开并读取文件。该方法简单明了;它会构造一个 FileStream 对象,分配 Byte[],调用 Read,然后处理返回的数据。C# using 语句可确保完成数据处理后会关闭该 FileStream 对象。

  ApmPatternWithMultipleMethods 方法显示了如何使用公共语言运行时 (CLR) 的 APM,来执行与 SynchronousPattern 方法相同的操作。您会立即看到实现过程要复杂得多。请注意,ApmPatternWithMultipleMethods 方法会启动异步 I/O 操作,操作完成时会调用 ReadCompleted 方法。同时请注意,两个方法之间的数据传递是通过将共享数据封装到 ApmData 类的实例来完成的,为此我必须专门进行定义,以便启用这两个方法之间的数据传递。还应注意,不能使用 C# using 语句,因为 FileStream 是在一个方法中打开,然后在另一个方法中关闭的。为弥补这个问题,我编写了代码,用于在 ReadCompleted 方法返回之前显式调用 FileStream 的 Close 方法。

  ApmPatternWithAnonymousMethod 方法展示了如何使用 C# 2.0 称为匿名方法的新功能重新编写此代码,通过此功能您可以将代码作为参数传递到方法。它能有效地让您将一个方法的代码嵌入到另一个方法的代码中。(我在所著书籍“CLR via C#”(CLR 编程之 C# 篇)(Microsoft Press, 2006) 中详细说明了匿名方法。)请注意,ApmPatternWithAnonymousMethod 方法要简短得多,也更易于理解 — 在习惯使用匿名方法后就可以体会到这一点。

  首先,请注意该代码较简单,因为它完全包含在一个方法内。在此代码中,我将调用 BeginRead 方法启动异步 I/O 操作。所有 BeginXxx 方法会将其第二个至最后一个参数视为一个引用方法的委托,即 AsyncCallback,该方法在操作完成时由线程池线程进行调用。通常,使用 APM 时,您必须编写单独的方法,为该方法命名,并通过 BeginXxx 方法的最后一个参数将额外数据传递到该方法。但是,匿名方法功能允许只编写单独的内嵌方法,这样启动请求和处理结果的所有代码便会和环境协调。实际上,该代码看上去与 SynchronousPattern 方法有些类似。

  其次,请注意 ApmData 类不再是必需的;您不需要定义该类、构造其实例以及使用它的任何字段!这是如何实现的?其实,匿名方法的作用不仅仅限于将一个方法的代码嵌入另一个方法的代码中。当 C# 编译器检测到外部方法中声明的任何参数或局部变量也用于内部方法时,该编译器实际上会自动定义一个类,并且两个方法之间共享的每个变量会成为此编译器定义的类中的字段。然后,在 ApmPatternWithAnonymousMethod 方法内,编译器会生成代码以构造此类的实例,且引用变量的任何代码都会编译成访问编译器所定义类的字段的代码。编译器还使得内部方法成为新类上的实例方法,允许其代码轻松地访问字段,现在两个方法可以共享数据。

  这是匿名方法的出色功能,它可让您像使用方法参数和局部变量一样编写代码,但实际上编译器会重新编写您的代码,从堆栈中取出这些变量,并将它们作为字段嵌入对象。对象可在方法之间轻松传递,并且可以从一个线程轻松迁移到另一个线程,这对于使用 APM 而言是十分完美的。由于编译器会自动执行所有的工作,您可以很轻松地将最后一个参数的空值传递到 BeginRead 方法,因为现在没有要在方法和线程之间显式传递的数据。但是,我仍然无法使用 C# using 语句,因为此处有两个不同的方法,尽管看上去似乎只有一个方法。

  以下内容显示了执行图 1 中摘录的代码后的输出。

  Primary ThreadId=1
  ThreadId=1: 4D-5A-90-00-03 (SynchronousPattern)
  ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithMultipleMethods)
  ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithAnonymousMethod)
  ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithLambdaExpression)
  ThreadId=3: 4D-5A-90-00-03 (ApmPatternWithIterator)

  我让 Main 方法显示应用程序主线程的托管线程 ID。然后我让 ProcessData 方法显示执行该方法的线程的托管线程 ID。如您所见,输出显示了所有异步模式让主线程之外的其他线程执行结果,而同步模式则让应用程序的主线程执行所有工作。

  还应指出,C# 3.0 引入了一个新功能,称为 lambda 表达式。在执行同一操作时,lambda 表达式功能的语法比 C# 匿名方法功能更简洁。实际上,这对于 C# 团队来说是一个麻烦,因为现在它必须记录和支持产生相同结果的两个不同语法。为了使用 C# 3.0 lambda 表达式功能,ApmPatternWithAnonymousMethod 方法经修改后成为图 1 中所示的 ApmPatternWithLambdaExpression 方法。在此处可以看到语法略为简化,因为编译器能够自动推断出结果参数的类型为 IAsyncResult,而且“=>”要键入的内容比“delegate”少。

  foreach 语句

  C# 2.0 为 C# 编程语言引入了另一种功能:迭代器。迭代器功能的最初目的是让开发人员能够轻松地编写代码,遍历集合的内部数据结构。要了解迭代器,必须首先好好看一下 C# foreach 语句。编写如下代码时,编译器会将它转化为如图 2 所示的内容:

  foreach (String s in collectionObject)
  DoSomething(s);

  可以看到,foreach 语句提供了遍历集合类中所有项目的简便方法。但是,有很多不同种类的集合类 — 数组、列表、词典、树、链接列表等等 — 每个均使用其内部数据结构来表示项目集合。使用 foreach 语句就是代表要遍历集合中的所有项目,而并不关注集合类内部用来维护其各个项目的数据结构。

  foreach 语句使得编译器生成对集合类的 GetEnumerator 方法的调用。此方法创建了一个实现 IEnumerator 接口的对象。此对象知道如何遍历集合的内部数据结构。while 循环的每次迭代都会调用 IEnumerator 对象的 MoveNext 方法。此方法告诉对象前进到集合中的下一个项目,如果成功,则返回 true,如果所有项目均已枚举,则返回 false。在内部,MoveNext 可能只会递增索引,也可能会前进到链接列表的下一个节点,或者它可能会向上向下遍历树。整个 MoveNext 方法的要点是,从执行 foreach 循环的代码中抽象出集合的内部数据结构。

  如果 MoveNext 返回 true,则调用 Current 属性的 get 访问器方法会返回 MoveNext 方法抵达项的值,以便 foreach 语句(try 块内部的代码)的主体能处理该项目。当 MoveNext 确定集合的所有项目均已得到处理时,会返回 false。

  此时,while 循环退出,并进入 finally 块。在 finally 块中,通过检查来确定 IEnumerator 对象是否实现 IDisposable 接口,如果是,则调用其 Dispose 方法。对记录而言,IEnumerator 接口由 IDisposable 派生而来,因此需要所有 IEnumerator 对象来实现 Dispose 方法。一些枚举器对象在迭代过程中需要附加资源。对象可能会返回文本文件的文本行。foreach 循环退出后(可能会在遍历集合中的所有项目之前发生),finally 块会调用 Dispose,允许 IEnumerator 对象释放这些附加资源 — 例如,关闭文本文件。


« 
» 
快速导航

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3