delphi 線程教學第四節:多線程類的改進


第四節:多線程類的改進
 
1.需要改進的地方
 
a) 讓線程類結束時不自動釋放,以便符合 delphi 的用法。即 FreeOnTerminate:=false;
b) 改造 Create 的參數,讓它適合訪問 COM 組件。如:在線程時空中能夠創建 TAdoConnection;
c) 設計一個接口能將一個過程( procedure )塞到線程時空中去運行的功能,這樣,不必每次重載 Execute 函數。
d) 設計一個輸出信息的接口
 
下一節,將講解如何用多個線程同時執行相同的任務
 
改進后的多線程類
本例源碼(delphi xe8版本)下載: FooThread.Zip
 
unit  uFooThread;
interface
uses
   System . Classes, System . SyncObjs;
 
type
   TOnMsg =  procedure (AMsg:  string of  object // 定義一個用於輸出信息的事件
 
   // 很多編程資料推薦在 String 參數前面加 const ,以提高效率
   // 我的理由是為了代碼美觀。如果有多個參數,加上 const 參數太長了。
   // 在以后的使用中,請自己斟酌是否加 const 。
   TFooThread =  class (TThread)
   private
     FEvent: TEvent;
     FCanAccessCom:  Boolean ;
     FRunningInThread: TThreadMethod;
     // TThreadMethod 的定義是 TThreadMethod = Procedure of object;
     // 意為這個 Procedure 是寫在一個類中的。
     // 在其它編程語言中,TThreadMethod 被稱為函數指針。
     // FRunningInThread 它用來保存將要在線程中運行的代碼或 Procedure
     procedure  DoExecute;
   protected
     // protected 段中定義的變量與函數,允許在子類中調用。
     procedure  Execute; override;
     procedure  DoOnStatusMsg(AMsg:  string );
     procedure  ExecProcInThread(AProc: TThreadMethod);
   public
     constructor  Create(ACanAccessCOM:  Boolean ); reintroduce;
     // reintroduce 是再引入 Create 的參數的意思。
     destructor  Destroy; override;
     procedure  StartThread; virtual;
   public
     OnStatusMsg: TOnMsg;
     // 亦可改寫為 Property OnStatusMsg:TOnMsg Read FOnMsg write SetOnMsg;
     // 太啰嗦了,如果不再對 SetOnMsg 進行操作,建議這樣寫。
     // 如果后期需要改動,原來的代碼亦可以不變。
   end ;
   // 未說明之處,請參考面向對象設計基礎知識。
 
implementation
 
uses  ActiveX, SysUtils;
 
constructor  TFooThread . Create(ACanAccessCOM:  Boolean );
begin
   inherited  Create( false );
   FEvent := TEvent . Create( nil true false '' );
   FreeOnTerminate :=  false ;
end ;
 
destructor  TFooThread . Destroy;
begin
   // 此處我們要設計手動 Free 的調用。
   Terminate;  // 首先要將 Terminated 設置為 true;
   FEvent . SetEvent;  // 啟動線程。
   WaitFor;  // 此 waitfor 的意思是等待線程退出 Execute
   // 此 WaitFor 是 TThread 類的。注意與 FEvent.WaitFor 區別
   // 本質上,它們都是操作系統提供的信號的等待功能。
   // 有興趣可以直接參考系統源碼 ( delphi 提供的源碼 )
   FEvent . Free;
   inherited ;
end ;
 
procedure  TFooThread . DoExecute;
begin
   FEvent . WaitFor;
   FEvent . ResetEvent;
   while  not  Terminated  do
   begin
 
     try
       FRunningInThread;  // 因為它是一個 Procedure ,故可直接運行。
     except
       // 捕捉異常,否則異常發生時代碼將退出 Execute ,線程生命周期就結束了。
       on  e: Exception  do
       begin
         DoOnStatusMsg( 'ThreadErr:'  + e . Message);
       end ;
     end ;
 
     FEvent . WaitFor;
     FEvent . ResetEvent;
 
   end ;
end ;
 
procedure  TFooThread . DoOnStatusMsg(AMsg:  string );
begin
   // 這是引發事件常用的寫法。
   if  Assigned(OnStatusMsg)  then
     OnStatusMsg(AMsg);
end ;
 
procedure  TFooThread . ExecProcInThread(AProc: TThreadMethod);
begin
   FRunningInThread := AProc;
   FEvent . SetEvent;  // 啟動線程。
   // 需要說明的是,第一次運行本函數 ExecProcInThread 一般是在主線程時空里運行。
   // 第二次運行本函數可以設計為在線程時空中運行,后面章節會講到。
  // 其作用是把 AProc 塞到線程時空中並啟動線程。
end ;
 
procedure  TFooThread . Execute;
begin
 
   if  FCanAccessCom  then
   begin
     CoInitialize( nil );
     // 在線程中初始化 COM ,反正調用了此句,才能在線程中使用 COM
     // 這是 windows 操作系統規定的,與 delphi 沒有關系。
     // 你用 api 操作線程,在線程中訪問 COM 同樣需要這樣做。
     try
       DoExecute;
     finally
       CoUninitialize;  // 與初始化對應,解除線程訪問 COM 的能力。
     end ;
   end
   else
     DoExecute;
end ;
 
procedure  TFooThread . StartThread;
begin
end ;
 
end .
 
先基於 TFooThread 繼承,代碼如下。
 
unit  uCountThread;
interface
uses
   uFooThread;
type
   TCountThread =  class ;
   TOnCounted =  procedure (Sender: TCountThread)  of  object ;
   TCountThread =  class (TFooThread)
   private
     procedure  Count;
     procedure  DoOnCounted;
   public
     procedure  StartThread; override;
   public
     Num:  integer ;
     Total:  integer ;
     OnCounted: TOnCounted;
   end ;
 
implementation
 
{ TCountThread }
 
procedure  TCountThread . Count;
var
   i:  integer ;
begin
   DoOnStatusMsg( '開始計算...' );
   Total :=  0 ;
   if  Num >  0  then
     for  i :=  1  to  Num  do
     begin
       Total := Total + i;
       sleep( 10 );  // 故意變慢,實際代碼請刪除此行。
       // 實際上為確保線程能夠及時退出
       // 此處還應加上一個判斷是否出的標志,請大家自行思考。
       // 這又是一個兩難的選擇。
       // 加了判斷標志,退出容易了,但效率又低了。
       // 所以,編程人員總是在效率與友好性中做出選擇。
       // 且編且珍惜。
     end ;
   DoOnCounted;  //引發 OnCounted 事件,告知調用者。
   DoOnStatusMsg( '計算完成...' );
end ;
 
procedure  TCountThread . DoOnCounted;
begin
   // if Assigned(OnCounted) then
   // 等價於 if OnCounted <> nil then
   if  Assigned(OnCounted)  then
     OnCounted(self);
end ;
 
procedure  TCountThread . StartThread;
begin
   inherited ;
   ExecProcInThread(Count);  // 把 Count 過程塞到線程中運行。
end ;
 
end .
 
是不是簡短很多?下面是調用。
 
unit  uFrmMain;
 
interface
 
uses
   Winapi . Windows, Winapi . Messages, System . SysUtils, System . Variants, System . Classes, Vcl . Graphics,
   Vcl . Controls, Vcl . Forms, Vcl . Dialogs, Vcl . StdCtrls, uCountThread;
 
type
   TFrmMain =  class (TForm)
     memMsg: TMemo;
     edtNum: TEdit;
     btnWork: TButton;
     procedure  FormCreate(Sender: TObject);
     procedure  FormDestroy(Sender: TObject);
     procedure  btnWorkClick(Sender: TObject);
   private
     { Private declarations }
     FCountThread: TCountThread;
     // 取名是一直是個有技術含量的事情。
     // 推薦去掉類名的 T 換成 F 這樣的寫法。
     procedure  DispMsg(AMsg:  string );
     procedure  OnThreadMsg(AMsg:  string );
     procedure  OnCounted(Sender: TCountThread);
   public
     { Public declarations }
   end ;
 
var
   FrmMain: TFrmMain;
 
implementation
 
{ $R  *.dfm}
{ TFrmMain }
 
procedure  TFrmMain . btnWorkClick(Sender: TObject);
var
   n:  integer ;
begin
   btnWork . Enabled :=  false ;
   n := StrToIntDef(edtNum . Text,  0 );
   FCountThread . Num := n;
   FCountThread . StartThread;
end ;
 
procedure  TFrmMain . DispMsg(AMsg:  string );
begin
   memMsg . Lines . Add(AMsg);
end ;
 
procedure  TFrmMain . FormCreate(Sender: TObject);
begin
   FCountThread := TCountThread . Create( false );  // 此處不需要訪問 Com 所以用 false
   FCountThread . OnStatusMsg := self . OnThreadMsg; 
// 因為是在線程時空中引發的消息,故這里用了 OnThreadMsg;
   FCountThread . OnCounted := self . OnCounted;
end ;
 
procedure  TFrmMain . FormDestroy(Sender: TObject);
begin
   // 這里要注意,盡管我們在 TFooThread 中的析構函數中
   // 寫了保證線程退出的函數。那也只是以防萬一的。
   // 在線程手動 Free 之前,一定要確保線程代碼已經退出了 Execute
 
   // 為了友好退出,又需要在計算代碼中加入判斷是否退出的標志。
   // 請參考 TCountThread Count 中的注釋。
 
   // 本教程一直反復強調“代碼退出Execute”這個概念。
   // 用線程,就得負責一切,不可偷懶!
 
   FCountThread . Free;
end ;
 
procedure  TFrmMain . OnCounted(Sender: TCountThread);
var
   s:  string ;
begin
   s := IntToStr(Sender . Num) +  '累加和為:' ;
   s := s + IntToStr(Sender . Total);
   OnThreadMsg(s);  // 因為這里是線程空間,所以需要用本函數。
   // 而不是 DispMsg;
   // 網絡組件,它的數據到達事件,其實是線程時空。要顯示信息
   // 也需要 Synchronize; 這是很多初學者易犯的錯誤。
   // 如果在線程時空中,不用 Synchronize 來操作 UI,就會出現時靈時不靈的狀態。
   // 初學者所謂的運行不穩定,調試時又是正常。往往原因就是如此。
 
   TThread . Synchronize( nil ,
     procedure
     begin
       btnWork . Enabled :=  true // 恢復按鈕狀態。
     end );
 
end ;
 
procedure  TFrmMain . OnThreadMsg(AMsg:  string );
begin
   TThread . Synchronize( nil ,
     procedure
     begin
       DispMsg(AMsg);
     end );
end ;
 
end .
 
 
 
 
  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM