以 Delphi 撰寫 Neutral 執行緒模型的 COM+ 元件

作者:蔡煥麟
日期:Feb-1-2001


簡介

目前坊間關於 COM+ 的中英文書籍可說是琳瑯滿目,相信很多人都已經知道 Windows2000 增加了一種叫做 Neutral Apartment 的執行緒模型,但是卻沒有人介紹如何以 Delphi 5 來撰寫這類元件,這篇文章會的目的就是希望能把這部分的學習心得與各位分享。閱讀這篇文章時,你應該具備以下知識或技能:

Neutral Apartment 執行緒模型

Windows NT4 支援的執行緒模型有 Single,Apartment(Single Threaded Apartment, STA)及 Free(Multi-threaded apartment, MTA),Delphi 5 也直接支援這些執行緒模型。而 Windows2000 加入了新的 Neutral Apartment(NA),這種新的執行緒模型可以讓你設計出來的元件真的享有 object pooling(物件集區)的好處--這麼說,難道我們目前所撰寫的元件都沒有用到 object pooling?

幾乎是的。首先,object pooling 在 MTS 2.0 中並沒有提供,也就是必須在 Windows2000 平台才有;其次,平常我們以 Delphi 5 或 VB6 所撰寫的元件絕大部分都是使用 STA 執行緒模型(VB6 只支援 STA),而 STA 無法使用 object pooling,只有 MTA 和 NA 才有支援 object pooling,但是 MTA 執行緒的元件在實作上比較複雜,所以在 Windows2000 平台上採用 NA 執行緒模型會是比較好的選擇。Delphi 5 雖然沒有直接支援 NA,但是透過繼承現有的類別再加上一些程式碼,就可以設計出具有 object pooling 功能的元件,稍後你就會看到,這項任務並不困難。

關於 Neutral Apartment,你可以看看微軟的 MSDN 網站怎麼說

Object Pooling

在動手之前,我想應該先解釋一下什麼是 object pooling,以及為什麼要使用它。Object pooling 的目的是希望在大量用戶端存取的情況下,縮短元件在建立及初始化所需要的時間。在分散式運算環境中,object pooling 可以有效的降低應用程式伺服器的負荷,並且加快應用程式伺服器對用戶端的回應時間,因為當物件被用完以後會被放入"物件池"裡面,等下次要使用時就不再重新建立,只是從物件池裡面取出來用,所以能夠省下建立物件的時間。但是請記住,object pooling 不是萬靈丹,它不會讓原本就跑得像烏龜的系統在效能上有戲劇性的進展,你在設計元件時就要先思考哪些元件需要,哪些元件不需要 object pooling。如果元件在建立及初始化時必須花費較多的系統資源及時間,你才能夠明顯的感受到 object pooling 的好處。

關於 Object pooling 如何增進效能的進一步資料,可以到微軟的 MSDN 網站看看

撰寫元件

有了基礎概念後,我們可以開始進入實作階段了。先啟動 Delphi,點選 File | New,然後在 New Items 對話盒中切換到 ActiveX 頁夾,點選 ActiveX Library,然後按一下 Ok 鈕。這時候你可以先存檔並且將專案命名為 D5Neutral.dpr。

再次點選 File | New,然後在 New Items 對話盒中切換到 Multi-tier 頁夾,點選 MTS Object,然後按一下 Ok 鈕。接下來會出現 New MTS object 對話盒,CoClassName(類別名稱)輸入 "NeutralObj",按一下 Ok 鈕,然後存檔並且將單元命名為 D5NeutralImpl.pas。

現在,我們要撰寫一個新的類別工廠(呃....類別工廠,簡單的說就是建立元件的元件,如果你想進一步了解的話,請閱讀文後所附的參考資料)。這個類別工廠主要的工作在於修改視窗的註冊資料庫(registry)以將元件的執行緒模型登錄成為 "Neutral"。做法是 New 一個 Unit,然後撰寫程式碼如下(這個程式碼片段的原始作者是 Max Alexeyev ):

unit NAObjFac;

interface

uses
  ActiveX, MtsObj, Mtx, ComObj, StdVcl;

type
  TNeutralAutoObjectFactory = class(TAutoObjectFactory)
  private
    FNeutralApartment: Boolean;
  public
    constructor Create(ComServer: TComServerObject; AutoClass: TAutoClass;
    const ClassID: TGUID; Instancing: TClassInstancing;
      ThreadingModel: TThreadingModel = tmSingle;
      ANeutralApartment: Boolean = False);
    procedure UpdateRegistry(Register: Boolean); override;
  end;


implementation

uses ComServ;

{ TNeutralAutoObjectFactory }

constructor TNeutralAutoObjectFactory.Create(ComServer: TComServerObject;
  AutoClass: TAutoClass; const ClassID: TGUID;
  Instancing: TClassInstancing; ThreadingModel: TThreadingModel;
  ANeutralApartment: Boolean);
begin
  inherited Create(ComServer, AutoClass, ClassID, Instancing, ThreadingModel);
  FNeutralApartment := ANeutralApartment;
end;

procedure TNeutralAutoObjectFactory.UpdateRegistry(Register: Boolean);
var
  sClassID, sServerKeyName: string;
begin
  inherited UpdateRegistry(Register);
  if Register and FNeutralApartment then
  begin
    if Instancing = ciInternal then
      Exit;
    sClassID := GUIDToString(ClassID);
    sServerKeyName := 'CLSID\' + sClassID + '\' + ComServer.ServerKey;
    if (ThreadingModel <> tmSingle) and IsLibrary then
      CreateRegKey(sServerKeyName, 'ThreadingModel', 'Neutral');
  end;
end;

end.

然後把 D5NeutralImpl.pas initialization 區段中原本由 Delphi 幫你產生的程式碼:

  TAutoObjectFactory.Create(ComServer, TNeutralObj, Class_NeutralObj,
    ciMultiInstance, tmApartment);

改成這樣:

  // 這個元件同時支援Win98/NT4 Win2000
  if IsWin2K then
    TNeutralAutoObjectFactory.Create(ComServer, TNeutralObj, Class_NeutralObj,
        ciMultiInstance, tmApartment, True);
  else
    TAutoObjectFactory.Create(ComServer, TNeutralObj, Class_NeutralObj,
      ciMultiInstance, tmApartment);

為了實驗看看 object pooling 確實有發揮作用,請在 TNeutralObj 裡面改寫 Initialize 方法,我在裡面加入了延遲約三秒鐘的程式碼:

procedure TNeutralObj.Initialize;
  procedure Delay(lMilliSeconds: longint);
  var
    lStart: longint;
  begin
    lStart := GetTickCount;
    while longint(GetTickCount) - lStart <= lMilliSeconds do
      Application.ProcessMessages;
  end;
begin
  inherited;
  Delay(3000);
end;

最後,加入一個簡單的 GetName 方法:

function TNeutralObj.GetName: string;
begin
  Result := 'TNeutralObj.GetName';
  SetComplete;
end;

請記得 COM+ 元件的方法在結束之前必須呼叫 SetCompleteSetAbort 才能將元件所佔用的資源釋放掉。

設定元件組態

將元件編譯連結,並且註冊,你可以利用 Delphi 的 IDE 提供的 Run | Install MTS Object 來註冊,或者使用〔元件服務〕管理工具來註冊元件。完了嗎?不,元件註冊完成後,只是具備了 object pooling 的功能,但是還沒啟用,你必須使用〔元件服務〕為元件啟用這項功能。做法是,在元件名稱上面點一下滑鼠右鍵,選內容,接著切換到〔啟動〕頁夾,將 "啟用物件集區" 打勾,"集區最小為" 輸入 2(也就是第一次建立時就在集區裡面建立 2 個物件), "啟用 Just In Time 啟動" 也必須打勾。參考下圖:

元件的部分到這裡就大功告成了,接下來你可以撰寫一個用戶端程式,建立並呼叫此元件的方法,以實際觀察第一次建立元件和之後建立元件所需的時間相差多少,在我的機器上執行的結果是,第一次呼叫元件的方法時需要 3 秒多,第二次以後就只需要 0.1 秒左右 。撰寫用戶端程式的這個部份就不贅述了,您可以下載完整的範例程式回去試試看。

下載範例程式(解開 zip 檔之後,請以 Delphi 開啟 BuildAll.bpg)

結語

簡單與功能強大經常是相互衝突的,以往程式員為了避免撰寫複雜的 MTA 程式碼,而選擇執行效率比較差的 STA,現在由於 Neutral Apartment 的出現終於有了一個最好的選擇。希望透過本文的介紹及實作練習之後,各位也能很輕易的以 Delphi 5 設計出具備 object pooling 的 COM+ 元件,並且對於如何改善分散式應用程式的執行效率有更多的認識。

參考資料