第三章 多线程
古时候,有一位刚刚出道的的骑士去到牧马场挑选一匹好马。在马房和牧马人聊天的时候,他大吹特吹自己驾驭马匹的高超技能。牧马人听完他的唠叨之后说:"请你将草原上吃草的那群马引进马房,我送你一匹最好的马!"。击掌为誓之后,骑士拿起长鞭骑马出去了。过了很久,那个骑士汗流满面灰溜溜地回来了。这时牧马人语重心长地对他说:"能驾驭一匹马不一定可以驾驭一群马,你在马背上的前程还长着呢!"。骑士听了之后羞愧满面。多年以后,这位骑士成为了一位领兵打仗的将军,驰骋沙场为国家立了不少战功。
能驾驭一匹马不一定可以驾驭一群马,会编写单线程的程序不一定会编写多线程的程序。编写多线程的程序就像驾驭一群马一样,要想每一匹马都能听你的话,可真是要下点功夫才行。
第一节 了解线程
线程就是程序执行的动态线索。和进程的概念一样,线程也是动态的。不过进程的概念偏向进程空间的动态性,而线程的概念更关注程序执行线索的动态性。
前面我们说过,推动进程运转的是线程。因为,Windows操作系统是以进程为单位来安排系统的空间,却以线程为单位来分配CPU的时间片。
操作系统在加载和执行一个程序时,首先为程序建立一个进程提供进程空间,然后再为程序建立一个线程以分配CPU时间片,推动程序的运行。这个由操作系统初始建立的线程就称为进程的主线程。
你发出运行程序的命令给操作系统,操作系统便安排内存空间等系统资源,并立即指派一个线程牵头去完成任务。这个牵头的线程可以独立完成你的任务,也可以再指派其他的线程协助完成任务。如果一个线程还指派了其他的线程一起来执行程序,这就是所谓的多线程。一个线程可以再指派一个或多个子线程,这些线程共同协作完成最终的任务。这就好像部门领导接到工作任务,他一般会在自己工作的同时安排任务给下属人员完成一样。当然,下属人员又可以安排任务给再下属的人员。这就是说,子线程还可产生它自己的子线程。
我很少编写多线程的程序,一般都是在前人搭建好的多线程的程序体系中,小心控制程序资源的共享和互斥问题。我想可能许多朋友都和我一样,很少自己去使用CreateThread函数或者用TThread类去建立一个线程。例如,在编写多层应用程序的服务器端对象时,常常需要选择一种线程模式,之后你只需要在程序里考虑在多线程情况下要注意的问题。DELPHI已经帮你搭建好了多线程的体系结构,只是你要清楚地认识到你正在编写多线程的程序。
在DELPHI的调试状态下,你是可以观察线程状态的。打开View\Debug Windows\Threads菜单可调出Thread Status窗口,在此可了解当前调试的程序有多少个线程以及它们的状态。下面是一个小程序,很简单,看起来绝对不是一个多线程的程序。但它使用了TAnimate控件,而一个TAnimate是用线程来实现的。写这个程序的目的主要是让你了解怎样用DELPHI的Thread Status窗口察看程序线程的变化。
项目文件AnimateThread.dpr:
program AnimateThread;
uses
Forms,
AnimateThreadUnit in 'AnimateThreadUnit.pas' {fAnimateThread};
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TfAnimateThread, fAnimateThread);
Application.Run;
end.
单元文件AnimateThreadUnit.pas:
unit AnimateThreadUnit;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ComCtrls, StdCtrls;
type
TfAnimateThread = class(TForm)
aniFindComputer: TAnimate;
aniFindFile: TAnimate;
chkFindComputer: TCheckBox;
chkFindFile: TCheckBox;
procedure chkFindComputerClick(Sender: TObject);
procedure chkFindFileClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
fAnimateThread: TfAnimateThread;
implementation
{$R *.DFM}
procedure TfAnimateThread.chkFindComputerClick(Sender: TObject);
begin
aniFindComputer.Active:=chkFindComputer.Checked;
end;
procedure TfAnimateThread.chkFindFileClick(Sender: TObject);
begin
aniFindFile.Active:=chkFindFile.Checked;
end;
end.
窗体文件AnimateThreadUnit.dfm:
object fAnimateThread: TfAnimateThread
Left = 578
Top = 112
Width = 187
Height = 96
Caption = '观察动画线程'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object aniFindComputer: TAnimate
Left = 16
Top = 12
Width = 16
Height = 16
Active = False
CommonAVI = aviFindComputer
StopFrame = 8
end
object aniFindFile: TAnimate
Left = 16
Top = 36
Width = 16
Height = 16
Active = False
CommonAVI = aviFindFile
StopFrame = 8
end
object chkFindComputer: TCheckBox
Left = 80
Top = 12
Width = 89
Height = 17
Caption = 'Find Computer'
TabOrder = 2
OnClick = chkFindComputerClick
end
object chkFindFile: TCheckBox
Left = 80
Top = 36
Width = 89
Height = 17
Caption = 'Find File'
TabOrder = 3
OnClick = chkFindFileClick
end
end
在DELPHI开发环境中运行这个程序,然后打开Thread Status窗口。这时,Thread Status窗口中只有一个线程的状态,那就是主线程。当你分别启动或停止程序窗口中的两个动画时,你会看到Thread Status窗口中会有另外两个线程产生或消失。如下图所示:
这个程序还让我们明白,我们在编写程序时有可能在不经意的情况下涉及了多线程。因为我们常用的一些控件可能就是用线程实现的,例如TAnimate。所以,多线程的程序可能无处不在,多了解和学习多线程的知识可以使我们在编程中少犯错误。
要编好多线程的程序,关键是控制好各线程之间的协同运作。说起来容易做起来难,协同运作可不那么容易。
我们知道,在Windows操作系统中,每一个进程有自己独立的进程空间。操作系统将运行的每一个进程严格的分隔开,进程间不会相互干扰。但进程中的多个线程就不是这样了,他们处在同一个生存空间之中,共享着进程空间的各种资源。如果不能有效地管束你创建的多个线程,他们就会为了争夺有限的进程资源而互相拼杀。一个线程好不容易计算出一组数据,另一个线程又将其改掉;一个线程刚打开一个文件,另一个线程立刻写了一些自己的意见到文件中;一个线程正在打印数据,而另一个线程中途插进一段它的表格。一切都乱了,最后的结果就是让你的整个程序死掉。
进程中的多个线程共享着进程的各种资源,但线程也不完全是一无所有,他也有自己的私有财产。在创建每一个线程的时候,操作系统也会在进程空间中为他划定一些自留地,让他拥有独立堆栈空间、独立的线程数据空间、独立的窗口消息队列和独立的异常处理链。一般来说,这些自留地是线程可以自己掌管的,不用担心其他线程来抢夺(除非其他线程不小心侵犯了这些自留地)。
对于共享的进程资源的使用,操作系统为我们提供了多种管理设备,包括临界区、互斥元、信号量和事件。你可以象使用隔离栏和交通信号灯控制马路上穿梭的车流一样,控制进程中的多个线程有序地运行。但关键是要制定好你的游戏规则,有了这么多管理设备不见得就能管好线程。
第二节 结构化异常处理
在这个世界上,没有人能够一点错误都不犯。再精明的人总又想不到的东西,所谓智者千虑终有一失,老虎也有打盹的时候。但关键是要看在遇到问题之后应怎样处理这些问题。
在结构化异常处理的思想和技术诞生以前,程序员们对程序运行中可能发生的各种异常一直没有一个比较好的方法。尽管,结构化的程序设计技术已经成熟并流行了许多年了。但在那时,即使是编写得最优秀的结构化程序,在遇到未想到的异常错误时,都有可能崩溃。异常错误处理就象一只拦路虎,阻碍程序员向更稳定可靠的程序前进。
那时候,程序员用得比较多异常错误处理方式就是:低层的程序尽可能多地判断所有可能发生的异常错误,并将发现的错误以错误状态的形式返回给上层程序。而上层的程序会针对返回的不同错误状态,进行相关的错误处理。这样,每一层的程序代码都会考虑下层程序会有些什么错误产生并且应该如何处理这些错误,而本层程序产生的错误又应该怎样通知到上一层的程序。最终,一个稳定可靠的优秀程序的代码就包含层层嵌套的if…then…else语句或case语句。在众多的逻辑判断分支程序中,只有一条分支是真正用得到的正确处理过程。于是,编写对异常错误处理的代码就远远多过对正确逻辑的处理代码。
想一想,当你好不容易才设计了一种复杂而又绝妙的处理问题的算法,可你又不得不在编写这一核心算法代码的同时,编写处理各种异常错误的代码,而且这些错误处理代码比核心算法的代码还要多!
有什么办法呢?那时我们就是这样熬过来的!几天几夜熬下来之后,终于可以欣慰地说:我的程序终于能运行了!
现在好了,有了结构化异常处理的思想和方法。这种思想和方法可以让你以一种标准的,也是机械的,当然也是合理的模式对待和处理各种异常错误。这种结构化错误处理思想也是非常简单的,基本符合人们对待日常错误的习惯做法。
比如,当上级领导给下属人员安排任务的时候,上级领导会要求下属人员尽力去完成任务。在大多数情况下任务是可以完成的,但上级领导必须考虑到如果下属人员不能完成任务应该怎么办。上级领导在处理不能完成任务的情况时,可以不必关心为什么不能完成任务而做出果断的处理,也可以弄清问题的具体原因而妥善解决。下属在执行任务中,由于某种原因而不能完成时,只需将问题抛给上级领导就可休息了。
结构化的异常错误处理方式要求调用中的每一层程序必须考虑两个问题,一是管好下层程序,二是对上层程序负责。不出问题则矣,一出问题就应该决定那些问题是本层程序必须处理,哪些问题是要提交高层程序处理,以确保职责分明。其实,我们在工作中不就是这样吗:管好下属人员,对高层领导负责,各尽其职,各担其责。
DELPHI中的结构化异常处理是用三种语句实现。
第一种是异常处理语句:
try
…… //执行任务代码
except
…… //异常处理代码
end
其执行流程是:执行try到except之间的代码,如果这些代码都执行成功,则执行end之后的其他代码;如果在try到except之间的代码时发生任何想象不到的异常错误时,将立刻转向except到end之间的代码进行异常处理,处理异常之后才接着执行end之后的其他代码。
第二种是异常保护语句:
…… //任务准备代码
try
…… //任务执行代码
finally
…… //异常保护代码
end
其执行流程是:在try之前的代码准备好执行所需的各种资源;然后开始执行try到finally之间的代码;不管try到finally之间的代码是否执行成功,都将执行finally到end之间的代码,以确保归还或释放前面准备的各种资源。如果,try到finally之间的代码执行成功,则执行完finally到end之间的代码之后,继续执行end之后的其他语句;否则,执行完finally到end之间的代码之后,异常将提交上层程序去处理错误。
第三种时异常产生语句:
raise ……
当程序判断出无法继续完成任务时,就产生一个异常。这时,raise语句之后的代码将不再执行,立刻转向上层的异常处理代码或异常保护代码。
结构化异常处理是可以嵌套的。"结构化"一词的含义就是从大处看是同一种结构,从细处看也是同一种结构;高层是这种结构,低层也是这种结构。也就是说,在try到except之间、except到end之间、try到finally之间、finally到end之间的代码中有可以嵌入另一个try…except…end或try…finally…end语句。这种嵌套不仅可以存在于同一过程或函数中,也可以上下跨越过程或函数的调用层次之间。
其实,DELPHI编写的整个应用程序的代码就是处在一个巨大的异常处理块之中的!一旦程序中出现的异常没有任何代码来处理,最终是由系统的异常处理代码处理的,结果就是中止应用程序。说得更深入一点,Windows中的任何线程的执行都是在一个操作系统提供的异常处理块之中!任何未被线程代码处理的异常最终由操作系统接管,结果就是中止和释放线程!
要知道,结构化异常处理是由操作系统来支持的,各种编程语言只是在此基础上描述自己的实现语法。结构化异常处理的控制流程和程序正常的控制流程是两套相对独立的控制流程。正常程序对调用层次的控制只需要堆栈机制就可以了,而结构化异常处理对层次的控制不仅需要堆栈,而且还需要一种称之为"结构化异常处理链"的数据结构。Windows操作系统在创建线程的时候,为每一个线程建立一个"结构化异常处理链",顶层链头上的异常处理就是中止和释放线程。我们又不是操作系统的专家,没有必要去研究操作系统是怎样实现神秘的结构化异常处理的。只要你知道有这么回事,就行了。
通常,try…except…end语句可以用在这些地方(我能想到的):
1. 将错误信息反馈给用户。例如:
try
Memo1.Lines.LoadFromFile('A:\README.TXT'); //可能发生问题的处理程序
except
ShowMessage('读取文件A:\README.TXT出错!'); //将错误情况反馈给用户。
end;
2. 对程序算法中的未知错误情况提供缺省值或经验值。例如:
try
ProfitRate:=(Income - Payout)/Income; //可能发生问题(Income=0时)的算法。
except
ProfitRate:=0.0; //返回缺省值或经验值。
end;
3. 出现程序错误时清除执行中的中间状态。例如:
Database.StartTransaction; //启动数据库事务。
try
…… //可能发生冲突的数据库修改代码。
Database.Commit;
except
Database.Rollback; //撤销数据库事务,清除中间状态。
raise; //再由上层去处理异常。
end;
通常,try…finally…end语句只用于资源的保护,我还没有想到其他的使用方法。例如:
aFile := FileOpen('LOG.DAT', fmOpenReadWrite); //打开文件
try
...... //使用文件操作代码。
finally
FileClose(aFile); //无论如何都要关闭文件。
end;
再如:
aDialog := TMyDialog.Create(nil); //建立对话框对象。
try
…… //使用对话框。
finally
aDialog.Free; //无论如何都要释放对话框对象。
end;
值得注意的是,如果程序用到多个资源时,你应该为每一个资源构造一个单独的资源保护结构代码,形成try…finally…end的嵌套结构。例如:
aFile := FileOpen('LOG.DAT', fmOpenReadWrite); //打开文件
try
……
aDialog := TMyDialog.Create(nil); //建立对话框对象。
try
…… //使用对话框。
finally
aDialog.Free; //无论如何都要释放对话框对象。
end;
...... //使用文件操作代码。
finally
FileClose(aFile); //无论如何都要关闭文件。
end;
千万别偷懒写成:
aFile := FileOpen('LOG.DAT', fmOpenReadWrite); //打开文件
aDialog := TMyDialog.Create(nil); //建立对话框对象。
try
...... //使用文件和对话框操作代码。
finally
aDialog.Free; //无论如何都要释放对话框对象。
FileClose(aFile); //无论如何都要关闭文件。
end;
因为,如果在文件打开之后,创建对话框发生异常,则打开的文件将不会关闭。
烦!真的很烦!最后你会发现一个非常稳定的异常处理嵌套可能是这样的:
try
......
try
......
try
......
except
......
end;
......
finally
......
try
......
except
......
end;
......
end;
......
except
......
try
......
except
......
raise;
end;
......
end;
其复杂的嵌套逻辑结构一点都不亚于早期处理错误的if…then..else和case的复杂嵌套结构!
也许,世界上的事情就是这样的吧,几经轮回总有相似的一幕。山林中的风声雨声,溪水流淌瀑布飞溅,鸟鸣兽吼,各种声音都是复杂的。但一曲《高山流水》同样描述这些复杂的风声水声和花香鸟语,却是悦耳动听。无序的声音是杂乱无章的噪音,而有序的声音便是和谐而美妙的音乐!
结构化异常错误处理虽然也很复杂,但却是有序的。它已经有了本质的飞跃,即使复杂也是另一层次的复杂。有序的复杂性是可以控制和把握的,虽然编程也很辛苦。但这时经过几天几夜熬下来,终于可以欣慰地说:我们的程序可以稳定运行了!