1 緣起
1.1 我的一個出錯程序
程序名稱:呼叫處理模塊的壓力測試工具,分為客戶端和服務端。
開發工具:Delhpi 5
相關技術:客戶端通過與服務端建立Socket連接來模擬一組電話機的撥入、按鍵、等待、掛機等過程。服務端對Socket事件以及收到的數據包進行預處理,並轉化為抽象的呼叫模型數據,然後發送給更上層的呼叫處理模塊。由於呼叫處理模塊是硬件無關的(與語音板卡、交換機類型均無關),因此通過此壓力測試工具可以比較真實地模擬海量呼叫,以達到測試呼叫處理模塊程序的邏輯正確性及其性能的目的。
由於系統設計時的某些考慮,該測試工具被分作客戶端和服務端兩個程序來實現,且采用socket進行通訊。現在想來,其實不如整合成一個程序實現更為簡單——但也正因為采用兩個程序來實現,才引發了後面的一些問題,並由此引入了簡單的垃圾回收框架。
1.2 問題
在測試工具的使用過程中,我們發現當呼叫量巨大,且測試工具動作頻繁的情況下,系統出現以下錯誤:
訪問地址錯(EAccessViolation),代碼地址位於$0046FC80附近,訪問地址多為$00000028。
出現EinvalidCast錯誤,該錯誤表明對一個地址進行類類型轉換時出錯(采用as關鍵字)。
程序內多處斷言失敗,出現許多引用已銷毀對象的情況。
仔細檢查程序後,我仍然認為這一切簡直是不可思議!而且,本來用於對別的程序進行測試的程序自身卻出現這類問題,幾乎讓我無地自容!
為了挽回自己的聲譽,我不得不成沉住氣來仔細跟蹤錯誤,排解問題!
2 解決辦法
2.1 查錯
其實問題的解決還比較順利。
通過查看程序的調用棧,發現程序出錯前總是停留在發送Socket數據包的過程裡。接著,進一步通過單步跟蹤,發現在發送數據包的過程中,Socket檢測到對端連接已經斷開,就會觸發OnDisconnect事件。而我正是在ServerSocket的OnDisconnect事件中根據傳遞進來的Socket句柄,找到對應的對象將之銷毀的。
我在ServerSocket的OnDisconnect事件中的代碼如下:
procedure Txxxx.ServerClientDisconnect(Sender: TObject;Socket: TCustomWinSocket);
Begin
…
FLines.DestroyLineBySocket(Socket);//正是這一句,在不合適的時機釋放了對象
…
End;
問題是這麼出現的。
比如,在某個過程中具有如下代碼(前面為行號):
1 FLine.DoSomething;
2 FLine.SendSocketData;
3 FLine.DoOtherThings;
其中,FLine是代表一路呼叫的對象。該對象內部引用了一個TCustomWinSocket指針。SendSocketData就是利用此Socket進行數據發送。
Flines是TLine對象的容器類的一個實例。
由此不難解讀前述的各類錯誤:
1. 由於行2的Socket連接斷開導致FLine對象釋放,因此行3訪問DoOtherThings幾乎必然造成訪問地址錯;
2. 由於行2的對象銷毀,因此程序中類似“Object as TLine”的代碼導致第二類錯誤;
3. 由於對象提前銷毀,善後處理工作未到位導致第三類錯誤;
2.2 解決方案
明白其原因後,問題解決起來就容易多了。
上述問題不外乎兩個方案:
一, 判斷實例是否存在
在DoOtherThings之後,判斷FLine對象是否仍然處於Flines之中,若是則繼續處理,否則結束處理;
二, 延遲銷毀FLine對象
在ServerSocket的OnDisconnect中,將FLine對象拋入垃圾池,待時機成熟時再銷毀。
考慮到方案一所要改動的代碼量較大,同時,此種方案代碼也不甚優美,因此決定采用方案二,即引入垃圾回收機制來解決問題。方案二的要點是選擇合適的時機真正銷毀對象。而對於這一點,問題倒不大,只需選擇消息循環中處理消息的第一個環節進行回收即可。因為在之後的處理環節中,必然能夠確保對FLine是否仍然有效的檢查。
3 簡易對象垃圾回收框架(untGarbagCollector)
3.1 概述
簡易的垃圾回收非常簡單:
使用TThreadList支持線程並發訪問,並保存待回收的對象指針;
提供Put方法保存待回收對象;
提供Recycle方法進行真正的回收(因為所有對象均自TObject派生而來)。
3.2 實現代碼
unit untGarbagCollector;
interface
uses
Classes;
type
TGarbagCollector = Class(TObject)
private
FList: TThreadList;
public
constructor Create;
destructor Destroy; override;
procedure Put(const AObject: TObject);
procedure Recycle(const MaxCount: Integer);
end;
function GarbagCollector: TGarbagCollector;
implementation
var
_GarbagCollector: TGarbagCollector;
function GarbagCollector: TGarbagCollector;
begin
if not Assigned(_GarbagCollector) then
_GarbagCollector := TGarbagCollector.Create;
result := _GarbagCollector;
end;
{ TGarbagCollect }
constructor TGarbagCollector.Create;
begin
FList := TThreadList.Create;
end;
destructor TGarbagCollector.Destroy;
begin
try
Recycle(FList.LockList.Count);
finally
FList.UnlockList;
end;
FList.Free;
end;
procedure TGarbagCollector.Put(const AObject: TObject);
begin
try
FList.LockList.Add(AObject);
finally
FList.UnlockList;
end;
end;
procedure TGarbagCollector.Recycle(const MaxCount: Integer);
var
I: Integer;
AList: TList;
begin
AList := FList.LockList;
try
I := 0;
while (AList.Count > 0) and (I < MaxCount) do
begin
TObject(AList.Last).Free;
AList.Delete(AList.Count - 1);
Inc(I);
end;
finally
FList.UnlockList;
end;
end;
initialization
finalization
if Assigned(_GarbagCollector) then
_GarbagCollector.Free;
end.
3.3 使用舉例
引用untGarbagCollector單元後,可以直接使用GarbagCollector進行對象的銷毀和回收。
銷毀
AObject := TObject.Create;
GarbagCollector.Put(AObject);
回收
可以在定時器、線程以及其他場合調用Recycle方法。
MaxCount是用於控制每次銷毀個數的參數,主要是怕一次性銷毀太多占用過多的cpu。
(突然發現還可以擴展為限制時間進行銷毀,比如每次銷毀耗時不超過的n毫秒)。
3.4 使用場合
在本案例中,為了防止對象過早銷毀引起訪問沖突,而引入了垃圾回收技術。
在其它場合,比如為了提高某些程序的主觀性能,也可以引入該技術。比如完成某些特定任務的程序,在處理過程中會產生臨時的對象,而銷毀這些對象又比較耗時。因此,為了盡早地結束任務,可以把這些臨時對象保存至垃圾池中。待作業(任務)完成,並且等一段時間後cpu比較空閒時,再把臨時對象真正銷毀。此做法的真谛就是以空間換取時間——與某些系統預創建對象,並重復利用對象以提高性能的做法相同。