程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> Delphi Thread

Delphi Thread

編輯:Delphi

TThread 詳解
        我們常有工作線程和主線程之分,工作線程負責作一些後台操作,比如接收郵件;

                                                              主線程負責界面上的一些顯示。

工作線程的好處在某些時候是不言而喻的,你的主界面可以響應任何操作,而背後的線程卻在默默地工作。


         VCL中,工作線程執行在Execute方法中,你必須從TThread繼承一個類並覆蓋Execute方法,在這個方法中,所有代碼都是在另一個 線程中執行的,除此之外,你的線程類的其他方法都在主線程執行,包括構造方法,析構方法,Resume等,很多人常常忽略了這一點。

最簡單的一個線程類如下:

TMyThread = class(TThread)
protected
procedure Execute; override;
end;

在Execute中的代碼,有一個技術要點,如果你的代碼執行時間很短,像這樣,Sleep(1000),那沒有關系;如果是這樣Sleep (10000),10秒,那麼你就不能直接這樣寫了,須把這10秒拆分成10個1秒,然後判斷Terminated屬性,像下面這樣:

procedure TMyThread.Execute;
var
   i: Integer;
begin
   for i := 0 to 9 do
      if not Terminated then
        Sleep(1000)
     else
        Break;
end;

這樣寫有什麼好處呢,

      想想你要關閉程序,在關閉的時候調用MyThread.Free,這個時候線程並沒有馬上結束,它調用WaitFor,等待 Execute執行完後才能釋放。

       你的程序就必須等10秒以後才能關閉,受得了嗎。如果像上面那樣寫,在程序關閉時,調用Free之後,它頂多再等一秒就 會關閉。

        為什麼?答案得去線程類的Destroy中找,它會先調用Terminate方法,在這個方法裡面它把Terminated設為True(僅此而 已,很多人以為是結束線程,其實不是)。

      請記住這一切是在主線程中操作的,所以和Execute是並行執行的。既然Terminated屬性已為 Ture,那麼在Execute中判斷之後,當然就Break了,Execute執行完畢,線程類也正常釋放。

或者有人說,TThread可以設FreeOnTerminate屬性為True,線程類就能自動釋放。除非你的線程執行的任務很簡單,不然,還是不要去理會這個屬性,一切由你來操作,才能使線程更靈活強大。

          接下來的問題是如何使工作線程和主線程很好的通信,很多時候主線程必須得到工作線程的通知,才能做出響應。比如接收郵件,工作線程向服務器收取郵件,收取完畢之後,它得通知主線程收到多少封郵件,主線程才能彈出一個窗口通知用戶。

在VCL中,我們可以用兩種方法,一種是向主線程中的窗體發送消息,另一種是使用異步事件。

第一種方法其實沒有第二種來得方便。想想線程類中的OnTerminate事件,這個事件由線程函數的堆棧引起,卻在主線程執行。

事實上,真正的線程函數是這個:
function ThreadProc(Thread: TThread): Integer;

函數裡面有Thread.Execute,這就是為什麼Execute是在其他線程中執行,該方法執行之後,有如下句:
Thread.DoTerminate;

而線程類的DoTerminate方法裡面是
if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);


顯然Synchronize方法使得CallOnTerminate在主線程中執行,而CallOnTerminate裡面的代碼其實就是:
if Assigned(FOnTerminate) then FOnTerminate(Self);

只要Execute方法一執行完就發生OnTerminate事件。不過有一點是必須注意,OnTerminate事件發生後,線程類不一定會釋 放,只有在FreeOnTerminate為True之後,才會Thread.Free。看一下ThreadProc函數就知道。

依照Onterminate事件,我們可以設計自己的異步事件。

Synchronize方法只能傳進一個無參數的方法類型,但我們的事件經常是要帶一些參數的,這個稍加思考就可以得到解決,即在線程類中保存參數,觸發事件前先設置參數,再調用異步事件,參數復雜的可以用記錄或者類來實現。

假設這樣,上面的代碼每睡一秒,線程即向外面引發一次事件,我們的類可以這樣設計:

 

[delphi] view plaincopyprint?TSecondEvent = procedure (Second: Integer) of object; 
TMyThread = class(TThread) 
private 
FSecond: Integer; 
FSecondEvent: TSecondEvent; 
procedure CallSecondEvent; 
protected 
procedure Execute; override; 
public 
property SencondEvent: TSecondEvent read FSecondEvent 
write FSecondEvent; 
end; 
 
{ TMyThread } 
 
procedure TMyThread.CallSecondEvent; 
begin 
if Assigned(FSecondEvent) then 
FSecondEvent(FSecond); 
end; 
 
procedure TMyThread.Execute; 
var 
i: Integer; 
begin 
for i := 0 to 9 do 
if not Terminated then 
begin 
Sleep(1000); 
FSecond := i; 
Synchronize(CallSecondEvent); 
end 
else 
Break; 
end;  
在主窗體中假設我們這樣操作線程: 
 
procedure TForm1.Button1Click(Sender: TObject); 
begin 
MyThread := TMyThread.Create(true); 
MyThread.OnTerminate := ThreadTerminate; 
MyThread.SencondEvent := SecondEvent; 
MyThread.Resume; 
end; 
 
procedure TForm1.ThreadTerminate(Sender: TObject); 
begin 
ShowMessage('ok'); 
end; 
 
procedure TForm1.SecondEvent(Second: Integer); 
begin 
Edit1.Text := IntToStr(Second); 
end; 
 
我們將每隔一秒就得到一次通知並在Edit中顯示出來。 
 
現在我們已經知道如何正確使用Execute方法,以及如何在主線程與工作線程之間通信了。但問題還沒有結束,有一種情況出乎我的意料之外,即如果 線程中有一些資源,Execute正在使用這些資源,而主線程要釋放這個線程,這個線程在釋放的過程中會釋放掉資源。想想會不會有問題呢,兩個線程,一個 在使用資源,一個在釋放資源,會出現什麼情況呢,  
 
用下面代碼來說明: 
 
type 
TMyClass = class 
private 
FSecond: Integer; 
public 
procedure SleepOneSecond; 
end; 
 
TMyThread = class(TThread) 
private 
FMyClass: TMyClass; 
protected 
procedure Execute; override; 
public 
constructor MyCreate(CreateSuspended: Boolean); 
destructor Destroy; override; 
end; 
 
implementation 
 
{ TMyThread } 
 
constructor TMyThread.MyCreate(CreateSuspended: Boolean); 
begin 
inherited Create(CreateSuspended); 
FMyClass := TMyClass.Create; 
end; 
 
destructor TMyThread.Destroy; 
begin 
FMyClass.Free; 
FMyClass := nil; 
inherited; 
end; 
 
procedure TMyThread.Execute; 
var 
i: Integer; 
begin 
for i := 0 to 9 do 
FMyClass.SleepOneSecond; 
end; 
 
{ TMyClass } 
 
procedure TMyClass.SleepOneSecond; 
begin 
FSecond := 0; 
Sleep(1000); 
end; 
 
end.  
 
用下面的代碼來調用上面的類: 
 
procedure TForm1.Button1Click(Sender: TObject); 
begin 
MyThread := TMyThread.MyCreate(true); 
MyThread.OnTerminate := ThreadTerminate; 
MyThread.Resume; 
end; 
 
procedure TForm1.Button2Click(Sender: TObject); 
begin 
MyThread.Free; 
end; 

TSecondEvent = procedure (Second: Integer) of object;
TMyThread = class(TThread)
private
FSecond: Integer;
FSecondEvent: TSecondEvent;
procedure CallSecondEvent;
protected
procedure Execute; override;
public
property SencondEvent: TSecondEvent read FSecondEvent
write FSecondEvent;
end;

{ TMyThread }

procedure TMyThread.CallSecondEvent;
begin
if Assigned(FSecondEvent) then
FSecondEvent(FSecond);
end;

procedure TMyThread.Execute;
var
i: Integer;
begin
for i := 0 to 9 do
if not Terminated then
begin
Sleep(1000);
FSecond := i;
Synchronize(CallSecondEvent);
end
else
Break;
end;
在主窗體中假設我們這樣操作線程:

procedure TForm1.Button1Click(Sender: TObject);
begin
MyThread := TMyThread.Create(true);
MyThread.OnTerminate := ThreadTerminate;
MyThread.SencondEvent := SecondEvent;
MyThread.Resume;
end;

procedure TForm1.ThreadTerminate(Sender: TObject);
begin
ShowMessage('ok');
end;

procedure TForm1.SecondEvent(Second: Integer);
begin
Edit1.Text := IntToStr(Second);
end;

我們將每隔一秒就得到一次通知並在Edit中顯示出來。

現在我們已經知道如何正確使用Execute方法,以及如何在主線程與工作線程之間通信了。但問題還沒有結束,有一種情況出乎我的意料之外,即如果 線程中有一些資源,Execute正在使用這些資源,而主線程要釋放這個線程,這個線程在釋放的過程中會釋放掉資源。想想會不會有問題呢,兩個線程,一個 在使用資源,一個在釋放資源,會出現什麼情況呢,

用下面代碼來說明:

type
TMyClass = class
private
FSecond: Integer;
public
procedure SleepOneSecond;
end;

TMyThread = class(TThread)
private
FMyClass: TMyClass;
protected
procedure Execute; override;
public
constructor MyCreate(CreateSuspended: Boolean);
destructor Destroy; override;
end;

implementation

{ TMyThread }

constructor TMyThread.MyCreate(CreateSuspended: Boolean);
begin
inherited Create(CreateSuspended);
FMyClass := TMyClass.Create;
end;

destructor TMyThread.Destroy;
begin
FMyClass.Free;
FMyClass := nil;
inherited;
end;

procedure TMyThread.Execute;
var
i: Integer;
begin
for i := 0 to 9 do
FMyClass.SleepOneSecond;
end;

{ TMyClass }

procedure TMyClass.SleepOneSecond;
begin
FSecond := 0;
Sleep(1000);
end;

end.

用下面的代碼來調用上面的類:

procedure TForm1.Button1Click(Sender: TObject);
begin
MyThread := TMyThread.MyCreate(true);
MyThread.OnTerminate := ThreadTerminate;
MyThread.Resume;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
MyThread.Free;
end;
 

先點擊Button1創建一個線程,再點擊Button2釋放該類,出現什麼情況呢,違法訪問,是的,MyThread.Free時,MyClass被釋放掉了

FMyClass.Free;

FMyClass := nil;

而此時Execute卻還在執行,並且調用MyClass的方法,當然就出現違法訪問。對於這種情況,有什麼辦法來防止呢,我想到一種方法,即在線程類中使用一個成員,假設為FFinished,在Execute方法中有如下的形式:

FFinished := False;
try
//... ...
finally
FFinished := True;
End;

接著在線程類的Destroy中有如下形式:

While not FFinished do
Sleep(100);
MyClass.Free;

這樣便能保證MyClass能被正確釋放。

             線程是一種很有用的技術。但使用不當,常使人頭痛。在CSDN論壇上看到一些人問,我的窗口在線程中調用為什麼出錯,主線程怎麼向其他線程發送消息等等,其實,我們在抱怨線程難用時,也要想想我們使用的方法對不對,

只要遵循一些正確的使用規則,線程其實很簡單。

後記

上面有一處代碼有些奇怪:FMyClass.Free; FMyClass := nil;如果你只寫FMyClass.Free,線程類還不會出現異常,即調用FMyClass.SleepOneSecond不會出錯。我在主線程中試了下面的代碼

MyClass := TMyClass.Create;
MyClass.SleepOneSecond;
MyClass.Free;
MyClass.SleepOneSecond;

同樣也不會出錯,但關閉程序時就出錯了,如果是這樣:

MyClass := TMyClass.Create;
MyClass.SleepOneSecond;
MyClass.Free;
MyThread := TMyThread.MyCreate(true);
MyThread.OnTerminate := ThreadTerminate;
MyThread.Resume;
MyClass.SleepOneSecond;

馬上就出錯。所以這個和線程類無線,應該是Delphi對於堆棧空間的釋放規則,

我想MyClass.Free之後,該對象在堆棧上空間還是保留 著,只是允許其他資源使用這個空間,

所以接著調用下面這一句MyClass.SleepOneSecond就不會出錯,當程序退出時可能對堆棧作一些清理 導致出錯。而如果MyClass.Free之後即創建MyThread,大概MyClass的空間已經被MyThread使用,所以再調用 MyClass.SleepOneSecond就出錯了。


 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved