第一章 简介
让我们从对c#5.0中async功能以及它对你有什么影响的概括介绍开始吧。
异步编程
如果代码开始执行了某个会长时间运行的操作,但却不等待该操作完成,它就是异步的。它的相反面是阻塞式代码,阻塞式代码会在操作执行期间一直等待而不做任何事情。
长时间运行的操作包括:
- 网络请求
- 磁盘访问
- 延迟一段时间
不同点在于运行代码的线程。在目前广泛使用的编程语言中,代码是运行在操作系统的线程中的。如果这个线程在长操作执行期间继续执行其他操作,那么你的代码就是异步的。如果线程仍然在运行你的代码,而不去执行其他操作,它就是阻塞式的,而你的代码也就是阻塞式代码。
当然,我们还有第三种策略来等待长操作的完成,那就是轮询(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时,他就会分割该方法。它做的事情相当复杂,这里我介绍一个假的框架,因为我发现考虑简单情况对于理解是很有用的。
- 把await之后的所有代码移到一个独立的方法
- 我们使用了一个新版本的DownloadString:DownloadStringAsync,它和原来的DownloadString做的一样,只是它是异步的。
- 我们把第二个方法传给它,在它执行完之后会调用这个方法。我们用了一些神奇的东西来做这件事情,在之后的章节中会做介绍。
- 当下载完成后,它将使用已下载的字符串回调我们,在这里,我们把该字符串输出到了控制台。
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语句块
- 函数的返回值
- 线程和上下文
- 性能
如果不理解实际发生了什么,你的程序将以你意想不到的方式失败,并且你不理解错误信息,调试器也不能解决它。