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就出錯了。