【IT168 技术】我们从构建响应灵敏的界面以及构建高可伸缩性的服务应用来讨论我们为什么需要异步编程,异步编程能给我们带来哪些好处。那么知道了好处,我们就开始吧,但是在异步编程这个方面,说总是比做简单。套用那句不是名言的名言:编写异步程序是困难的,编写可靠的异步程序尤其困难。因为异步程序非常难以编写,而且非常容易出错,很多基本的构造元素在异步编程中都无法使用,这让我们这些开发人员更愿意编写同步的代码,虽然我们知道有些地方真的应该使用异步。
如何实现异步
对于很多人来说,异步就是使用后台线程运行耗时的操作。在有些时候这是对的,而在我们日常大部分场景中却不对。
比如现在我们有这么一个需求:使用HttpWebRequest请求某个指定URI的内容,然后输出在界面上的文本域中。同步代码很容易编写:
2: {
3: var request = HttpWebRequest.Create("http://www.sina.com.cn");
4: var response = request.GetResponse();
5: var stream = response.GetResponseStream();
6: using(StreamReader reader = new StreamReader(stream))
7: {
8: var content = reader.ReadToEnd();
9: this.txtContent.Text = content;
10: }
11: }
是吧,很简单。但是正如上一篇文章所说,这个简短的程序体验会非常差。特别是在URI所指向的资源非常大,网络非常慢的情况下,在点击下载按钮到获得结果这段时间界面会假死。
哦,这个时候你想起了异步。回忆上篇文章的示意图。我们发现只要我们将耗时的操作放到另外一个线程上执行就可以了,这样我们的UI线程可以继续响应用户的操作。
使用独立的线程实现异步
如是你写下了下面的代码:
2: {
3: var downloadThread = new Thread(Download);
4: downloadThread.Start();
5: }
6:
7: private void Download()
8: {
9: var request = HttpWebRequest.Create("http://www.sina.com.cn");
10: var response = request.GetResponse();
11: var stream = response.GetResponseStream();
12: using(StreamReader reader = new StreamReader(stream))
13: {
14: var content = reader.ReadToEnd();
15: this.txtContent.Text = content;
16: }
17: }
然后,F5运行。很不幸,这里出现了异常:我们不能在一个非UI线程上更新UI的属性(更详细的讨论参见我的这篇文章:WinForm二三事(三)Control.Invoke&Control.BeginInvoke)。我们暂时忽略这个异常(在release模式下是不会出现的,但这是不推荐的做法)。
哦,你写完上面的代码后发现UI不再阻塞了。心里想,异步也不过如此嘛。过了一会儿你突然想起,你好像在哪本书里看到过说尽量不要自己声明Thread,而应用使用线程池。如是你搜索了一下MSDN,将上面的代码改成下面这个样子:
2: {
3: ThreadPool.QueueUserWorkItem((state) => {Download();});
4: }
5:
6: private void Download()
7: {
8: var request = HttpWebRequest.Create("http://www.sina.com.cn");
9: var response = request.GetResponse();
10: var stream = response.GetResponseStream();
11: using(StreamReader reader = new StreamReader(stream))
12: {
13: var content = reader.ReadToEnd();
14: this.txtContent.Text = content;
15: }
16: }
嗯,很容易完成了。你都有点佩服自己了,这么短的时间居然连线程池这么“高级的技术”都给使用上了。就在你沾沾自喜的时候,你的一个同事走过来说:你这种实现方式是非常低效的,这里要进行的耗时操作属于IO操作,不是计算密集型,可以不分配线程给它(虽然不算准确,但如果不深究的话就这么认为吧)。
你的同事说的是对的。对于IO操作(比如读写磁盘,网络传输,数据库查询等),我们是不需要占用一个thread来执行的。现代的磁盘等设备,都可以与CPU同时工作,在磁盘寻道读取这段时间CPU可以干其他的事情,当读取完毕之后通过中断再让CPU参与进来。所以上面的代码,虽然构建了响应灵敏的界面,但是却创建了一个什么也不干的线程(当进行网络请求这段时间内,该线程会被一直阻塞)。所以,如果你要进行异步时首先要考虑,耗时的操作属于计算密集型还是IO密集型,不同的操作需要采用不同的策略。对于计算密集型的操作你是可以采用上面的方法的:比如你要进行很复杂的方程的求解。是采用专门的线程还是使用线程池,也要看你的操作的关键程度。