第一章 简介

让我们从对c#5.0中async功能以及它对你有什么影响的概括介绍开始吧。

异步编程

如果代码开始执行了某个会长时间运行的操作,但却不等待该操作完成,它就是异步的。它的相反面是阻塞式代码,阻塞式代码会在操作执行期间一直等待而不做任何事情。

长时间运行的操作包括:

  1. 网络请求
  2. 磁盘访问
  3. 延迟一段时间

不同点在于运行代码的线程。在目前广泛使用的编程语言中,代码是运行在操作系统的线程中的。如果这个线程在长操作执行期间继续执行其他操作,那么你的代码就是异步的。如果线程仍然在运行你的代码,而不去执行其他操作,它就是阻塞式的,而你的代码也就是阻塞式代码。

当然,我们还有第三种策略来等待长操作的完成,那就是轮询(polling),在这种方式下,你重复的询问操作是否完成。虽然这种方式对于非常短的操作有一席之地,但是它通常是一个坏主意。

你可能在以前的工作中已经使用过异步代码。你使用的线程或者线程池(ThreadPool都是异步的,因为你启动新线程或者使用线程池的那个线程之后是自由的,它可以做一些其他的事情。如果你创建了一个Web页面,而这个页面运行用户从它访问其他页面,那么它也是异步的,因为没有线程在Server端等待用户的输入。这看起来可能很明显,但是想想写一个使用Console.ReadLine()来请求用户输入的控制台应用程序,并且你也可以想象一个针对Web的可选阻塞设计。它可能是一个非常糟糕的设计,但它确实是可行的。

编写异步代码的难点通常在于你需要知道那个长操作何时完成,然后做一些其他操作。这在阻塞式代码中非常容易,你只需在调用长操作的代码行后面写你的代码即可。在异步的情况下,这么做是不行的,因为下一行代码大部分情况下会在异步操作完成之前执行。

为了解决这个问题,我们发明了各种模式来在后台操作完成之后运行某些代码:

  • 在后台操作的主体代码之后,插入要执行的代码
  • 注册一个在后台操作完成之后触发的事件
  • 传递一个在后台操作完成之后执行的代理或者lambda表达

如果下一步的操作需要在特定的线程上执行(例如Winform和WPF中的UI线程),那么你需要把这个操作在那个线程上加入队列。这样做事非常麻烦的。

为什么异步代码这么好?

异步代码会释放启动它的线程,这在许多情况下都非常有用。一点好处是,线程会占用系统资源,使用较少的资源总是好的。而更多的情况是,只有一个线程可以做某些操作,像UI线程,如果你不尽快的释放它,你的应用程序就会失去响应性。下一章中,我们会讨论更多关于这方面的原因。

Async使用我激动不已的原因是它提供了利用并行编程优点的机会。Async使得你能够以新的方式来组织你的代码,使用更细粒度的并行,而不会使代码更复杂而难以维护。第10章将解释这个可能性。

什么是Async?

在C#5.0中,微软的编译器团队提供了一个强大的性能。它以两个新的关键字呈现:

  • async
  • await

它也依赖.Net framework 4.5的一些修改使得自身更加强大和有用。

Async是C#编译器的功能,而非以库的形式提供的。编译器会对你的源代码做一些类似C#早期版本中对lambda和迭代器的转换操作。

这个功能使得异步编程更加容易,因为它消除了在C#之前版本中必须使用的复杂模式。使用它,我们可以用异步的风格编写整个应用程序。本书中,我将用术语asynchronous指代C#新功能async带来的通用编程风格。异步编程早已存在于C#中,但是需要程序员做更多的手工工作。

Async做了什么

Async功能是表达长操作之后要执行什么操作的一种方式,而这种方式可读性好并且是异步执行的。

Async方法会被编译器转换为异步代码,而它却看起来和其对应的阻塞版本类似。下面是一个下载Web页面的阻塞方法。

private void DumpWebPage(string uri)

{

    WebClient webClient = new WebClient();

    string page = webClient.DownloadString(uri);

    Console.WriteLine(page);

}

下面是使用async的对应方法:

private async void DumpWebPageAsync(string uri)

{

    WebClient webClient = new WebClient();

    string page = await webClient.DownloadStringTaskAsync(uri);

    Console.WriteLine(page);

}

它们看起来非常相似,但是内部实现却是非常不同。

方法使用async关键字修饰,这是要在方法中使用await关键字所必须的。我们也在方法名后面加了Async后缀以遵守规范。

有趣的点事await关键字。但编译器看到await时,他就会分割该方法。它做的事情相当复杂,这里我介绍一个假的框架,因为我发现考虑简单情况对于理解是很有用的。

  1. 把await之后的所有代码移到一个独立的方法
  2. 我们使用了一个新版本的DownloadString:DownloadStringAsync,它和原来的DownloadString做的一样,只是它是异步的。
  3. 我们把第二个方法传给它,在它执行完之后会调用这个方法。我们用了一些神奇的东西来做这件事情,在之后的章节中会做介绍。
  4. 当下载完成后,它将使用已下载的字符串回调我们,在这里,我们把该字符串输出到了控制台。
private void DumpWebPageAsync(string uri)

{

    WebClient webClient = new WebClient();

    webClient.DownloadStringTaskAsync(uri) <- magic(SecondHalf);

}

private void SecondHalf(string awaitedResult)

{

    string page = awaitedResult;

    Console.WriteLine(page);

}

当运行这些代码时,当初的调用线程发生了什么事情呢?当它到达对DownloadStringTaskAsync的调用后,下载就开始了,但是不在该调用线程中。在该线程中,我们到达了方法的末尾并返回。至于线程下一步做什么取决于调用者。如果是UI线程,它将回去处理用户操作。否则它的资源将被释放。那意味着我们已经编写了异步代码。

Async不能解决所有问题

Async功能故意被设计成看起来和阻塞式代码尽可能的相似。我们可以处理长操作或者远程操作,就像它们是本地快速操作一样,又能获得异步调用的好处。

然而,它并不是为了让你忘记有后台操作,回调会发生而设计的。你必须小心处理许多在使用async时行为不同的事情:

  • 异常和try..catch...finally语句块
  • 函数的返回值
  • 线程和上下文
  • 性能

如果不理解实际发生了什么,你的程序将以你意想不到的方式失败,并且你不理解错误信息,调试器也不能解决它。

posted @ 2015-06-12 08:01  Fintech技术汇  阅读(133)  评论(0编辑  收藏  举报