第25章 异步编程和多线程编程
在平时的工作中,读者会经常遇到某些操作,诸如打印、下载等操作。这类操作都有一个共同点就是比较费时,都需要花费一定的时间才能完成。在程序中调用这类比较费时的代码时,调用方如果停在那里等待费时的代码执行完毕,无疑会严重影响程序的可操作性。例如,如果当打印或者下载进行时,如果我们只能等待不能做任何其他的事,甚至都不能取消打印或下载操作,那该是多么让人郁闷的事!再举个例子,现在很多应用程序将设置保存在配置文件中,那么当程序启动时,由于需要加载配置,然后利用这些配置数据进行一系列的初始化操作,但因为I/O读取操作稍慢,这将导致程序的主窗体不能立刻显示,给用户一种启动过程十分漫长的感觉,这显然不是一个好的用户体验。如今,实现某个功能已经不是最重要的事,大家都可以实现,但程序的可用性往往被用户给予了更多的关注,有时候甚至会对用户的采购决策起决定性的作用。这类问题,可以借助异步调用或者多线程编程模型轻松解决,如下是可能的解决方案,供大家参考:
❑把整个初始化处理放进一个单独线程,主线程启动此线程后继续执行其他操作,例如窗体绘制操作,当初始化配置数据的进行还在执行的同时,主窗体也快速地展现在用户眼前。虽然当前主窗体可能还不能完全可用,但给用户一种程序飞快运行的感觉,这种感觉无疑棒极了!
❑配置信息初始化线程此刻也在同步执行,将配置文件中的数据读取到内存,并根据配置对当前程序进行初始化。
截至目前,本书所给出的示例都是同步调用。什么是同步调用呢?就是程序按照在代码中既定的顺序,从开始一直顺序执行到结束。这也是有时候会造成之前所述的问题的根源所在,那么本章将致力于解决这一“影响用户心情”的问题。
25.1 进程和线程
什么是进程呢?在我们启动一个应用程序后,系统将会给它分配一定的内存以及其他的一些资源,这些划定的内存以及资源的物理分隔叫做进程。进程(process)是一块包含了某些资源的内存区域,操作系统利用进程把它的工作划分为一些功能单元。在Windows系统中,可以通过“Windows任务管理器”来查看当前运行的进程,可以通过在“开始”工具栏的空白处单击鼠标右键,从弹出的快捷菜单中选择“启动任务管理器”,或者使用快捷键"Ctrl+Shift+Esc"。在“任务管理器”窗口,会发现有5个选项卡:应用程序、进程、性能、联网以及用户。其中,在“进程”选项卡中显示了当前正在运行的进程名称,以及该进程使用CPU的情况、使用内存的情况以及进程所包含的线程数,如果这些信息不够,还可以通过单击“查看”菜单,然后单击“选择列”来查看更多的进程信息,如图25-1所示。
图 25-1 Windows任务管理器
从图25-1还可以看到,每个进程都包含一定数量的线程,例如ctfmon.exe有一个线程,devenv.exe(就是我们使用的Visual Studio 2010开发环境)却有27个线程。线程究竟是什么呢?线程是系统分配处理器时间资源的基本单元,或者说是进程之内的独立执行的一个单元。对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。从另一个角度来说,线程是由进程创建的,由处理器使用的一个执行序列。
再注意观察图25-1,“应用程序”和“进程”分属两个不同的选项卡,这说明它们是不同的,一个应用程序可能包含一个或多个进程,每个进程都拥有自己独立的数据、执行代码以及系统资源。如图25-2所示,打开Internet Explore8浏览器,会发现应用程序默认创建了2个进程,如果打开更多的选项卡,将会创建更多的进程。
图 25-2 Internet Explore 8浏览器的多个进程
理解进程和线程是进行异步编程的基础。目前为止,我们使用的都是同步编程的示例,所谓同步编程就是指,从第一条语句直到最后一条一句都是顺序执行。之前也讨论过,这在某些情况下会给用户造成困扰。例如,当在一个应用程序中向打印机发出了打印的指令,如果是同步模式,那么我们就不得不等待慢速的打印机执行完打印任务,此时应用程序是停止响应的,我们无法进行其他操作,也无法取消打印任务——这显然是不可接受的。
改进这一缺憾的方式,就是将同步编程改为异步编程。所谓异步编程,就是合理地利用多线程处理,从理论上讲,这些线程是“同时”执行的。这里的“同时”加了双引号,是因为当应用在一个单核处理器执行的时候,并不是真正地“同时执行”,而是操作系统会定期中断当前正在执行的线程,然后将CPU分配给等待队列中的另一个线程(这意味着任何一个线程都不能独占CPU),而具体每个线程所占用的CPU时间的长短取决于进程和操作系统。因为进程分配给每个线程的时间很短,以至于让我们产生“所有的线程是同时执行”的错觉。
实际上,如果你的应用是单线程的,那么即使运行在一个四核处理器上,它也是仅仅使用其中的一个内核,其余三个内核并不会用到。关于如何能够全面利用多核处理器并行处理优势,我们会在第29章介绍。
仍然以打印的例子为例,此时还在应用程序中发起一个打印的任务,但应用程序在向打印机发出打印指令后就立即将控制权返回给用户,具体的打印任务执行交给了另一个新的线程去做,此时我们可以在主线程中继续操作。如果你愿意,甚至可以随时中断打印线程的执行。此时,我们得到的是更高的工作效率和更好的用户体验。