悟透delphi 第三章 多線程
第三章 多線程
本書原著李戰(leadzen)大牛,由tingsking18整理,本人blog發布的版本經過戰哥同意,轉載請著名出處和原作者!
古時候,有一位剛剛出道的的騎士去到牧馬場挑選一匹好馬。在馬房和牧馬人聊天的時候,他大吹特吹自己駕馭馬匹的高超技能。牧馬人聽完他的嘮叨之后說:“請你將草原上吃草的那群馬引進馬房,我送你一匹最好的馬!”。擊掌為誓之后,騎士拿起長鞭騎馬出去了。過了很久,那個騎士汗流滿面灰溜溜地回來了。這時牧馬人語重心長地對他說:“能駕馭一匹馬不一定可以駕馭一群馬,你在馬背上的前程還長著呢!”。騎士聽了之后羞愧滿面。多年以后,這位騎士成為了一位領兵打仗的將軍,馳騁沙場為國家立了不少戰功。
能駕馭一匹馬不一定可以駕馭一群馬,會編寫單線程的程序不一定會編寫多線程的程序。編寫多線程的程序就像駕馭一群馬一樣,要想每一匹馬都能聽你的話,可真是要下點功夫才行。
第一節 了解線程
線程就是程序執行的動態線索。和進程的概念一樣,線程也是動態的。不過進程的概念偏向進程空間的動態性,而線程的概念更關注程序執行線索的動態性。
前面我們說過,推動進程運轉的是線程。因為,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的復雜嵌套結構!
也許,世界上的事情就是這樣的吧,幾經輪回總有相似的一幕。山林中的風聲雨聲,溪水流淌瀑布飛濺,鳥鳴獸吼,各種聲音都是復雜的。但一曲《高山流水》同樣描述這些復雜的風聲水聲和花香鳥語,卻是悅耳動聽。無序的聲音是雜亂無章的噪音,而有序的聲音便是和諧而美妙的音樂!
結構化異常錯誤處理雖然也很復雜,但卻是有序的。它已經有了本質的飛躍,即使復雜也是另一層次的復雜。有序的復雜性是可以控制和把握的,雖然編程也很辛苦。但這時經過幾天幾夜熬下來,終于可以欣慰地說:我們的程序可以穩定運行了!
- 上一篇 ?JQuery筆記
- 下一篇 ?Javascript 第三章