delphi多線程TThread詳解 (轉)

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章