使用Delphi7 COM接口进行多线程操作时的内存消耗问题

5
在使用多线程方式访问COM对象接口(如IXMLDocument和IXMLNode等)时,Delphi7存在一些内存问题。其他COM接口也可能存在此问题,但我的“研究”并不深入,因为我还要继续进行当前的项目。在单线程中创建TXMLDocument并通过IXMLDocument和IXMLNode等接口进行操作是可以的,但在多线程方法中,当一个线程创建TXMLDocument对象并且其他线程进行操作时,会使用越来越多的内存。每个线程都调用CoInitializeEx(nil, COINIT_MULTITHREADED),但无济于事。似乎每个线程在获取接口时都会分配一些内存并且不会释放它,但每个线程至少为某个接口(例如DocumentElement或ChildNodes)分配一次 - 因此除了创建对象的工作线程之外的一个工作线程不会导致可见的内存泄漏。但是动态创建的线程都以相同的方式行事,并最终消耗掉进程内存。以下是完整的测试应用程序Delphi7表单,作为显示上述三种不同情况的简化代码示例 - 单线程、一个工作线程和动态创建的线程。
unit uComTest;

interface

uses 
  Windows, SysUtils, Classes, Forms, ExtCtrls, Controls, StdCtrls, XMLDoc, XMLIntf,            ActiveX;

type

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

  TForm1 = class(TForm)

    btnMainThread: TButton;
    edtText: TEdit;
    Timer1: TTimer;
    btnOneThread: TButton;
    btnMultiThread: TButton;
    Timer2: TTimer;
    chkXMLUse: TCheckBox;

    procedure FormCreate(Sender: TObject);
    procedure btnMainThreadClick(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure btnOneThreadClick(Sender: TObject);
    procedure btnMultiThreadClick(Sender: TObject);
    procedure Timer2Timer(Sender: TObject);

  private

    fXML:TXMLDocument;
    fXMLDocument:IXMLDocument;
    fThread:TMyThread;
    fCount:Integer;
    fLoop:Boolean;

    procedure XMLCreate;
    function XMLGetItfc:IXMLDocument;
    procedure XMLUse;

  public

end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject); 
begin
  CoinitializeEx(nil, COINIT_MULTITHREADED);
  XMLCreate; //XML is created on MainThread;
  Timer1.Enabled := false;
  Timer2.Enabled := false;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  fIXMLDocument := nil;
  CoUninitialize;
end;

procedure TForm1.XMLCreate;
begin
  fXML := TXMLDocument.Create('.\try.xml');
  fXML.Active;
  fXML.GetInterface(IXMLDocument, fIXMLDocument);
end;

function TForm1.XMLGetItfc:IXMLDocument;
begin
  fXML.GetInterface(IXMLDocument, Result); 
end;

procedure TForm1.XMLUse;
begin
  Inc(fCount);

  if chkXMLUse.Checked then
  begin
    XMLGetItfc.DocumentElement;
    edtText.Text := IntToStr(GetCurrentThreadId) + ': ' + 'XML access  ' + IntToStr(fCount);
  end
  else
    edtText.Text := IntToStr(GetCurrentThreadId) + ': ' + 'NO XML access  ' +   IntToStr(fCount)
end;

procedure TForm1.btnMainThreadClick(Sender: TObject);
begin
  fCount := 0;
  fLoop := false;
  Timer1.Enabled := not Timer1.Enabled;
end;

procedure TForm1.btnOneThreadClick(Sender: TObject);
begin
  if fLoop then
    fLoop := false
  else
  begin
    fCount := 0;
    fLoop := true;
    fThread := TMyThread.Create(FALSE);
  end;
end;

procedure TForm1.btnMultiThreadClick(Sender: TObject);
begin
  fCount := 0;
  fLoop := false;
  Timer2.Enabled := not Timer2.Enabled;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  XMLUse;
end;

procedure TForm1.Timer2Timer(Sender: TObject);
begin
  TMyThread.Create(FALSE);
end;

//this procedure executes in every thread
procedure TMyThread.Execute;
begin
  FreeOnTerminate := TRUE;
  CoinitializeEx(nil, COINIT_MULTITHREADED);
  try
    repeat
      Form1.XMLUse;
      if Form1.floop then
        sleep(100);
    until not Form1.floop;
  finally
    CoUninitialize;
  end;
end;

end.

嗯,这很必要,因为这是一个带有按钮定时器的工作Delphi表单,但你不能只是复制和编译它。这里也有form的dfm:

object Form1: TForm1
  Left = 54
  Top = 253
  Width = 337
  Height = 250
  Caption = 'Form1'
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'MS Sans Serif'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = FormCreate
  OnDestroy = FormDestroy
  PixelsPerInch = 96
  TextHeight = 13
  object btnMainThread: TButton
    Left = 24
    Top = 32
    Width = 75
    Height = 25
    Caption = 'MainThread'
    TabOrder = 0
    OnClick = btnMainThreadClick
  end
  object edtText: TEdit
    Left = 24
    Top = 8
    Width = 257
    Height = 21
    TabOrder = 1
  end
  object btnOneThread: TButton
    Left = 24
    Top = 64
    Width = 75
    Height = 25
    Caption = 'One Thread'
    TabOrder = 2
    OnClick = btnOneThreadClick
  end
  object btnMultiThread: TButton
    Left = 24
    Top = 96
    Width = 75
    Height = 25
    Caption = 'MultiThread'
    TabOrder = 3
    OnClick = btnMultiThreadClick
  end
  object chkXMLUse: TCheckBox
    Left = 112
    Top = 88
    Width = 97
    Height = 17
    Caption = 'XML use'
    Checked = True
    State = cbChecked
    TabOrder = 4
  end
  object Timer1: TTimer
    Interval = 100
    OnTimer = Timer1Timer
  end
  object Timer2: TTimer
    Interval = 100
    OnTimer = Timer2Timer
    Left = 32
  end
end

下面是一个控制台应用程序。只需运行它,看看是否发生了任何内存消耗。如果您认为可以以保留多线程但不占用内存的方式编写,请随意修改它:

program ConsoleTest;

{$APPTYPE CONSOLE}

uses

  Windows, SysUtils, Classes, XMLDoc, XMLIntf, ActiveX;

type

  TMyThread = class(TThread)

    procedure Execute;override;

  end;

var
  fCriticalSection:TRTLCriticalSection;
  fIXMLDocument:IXMLDocument;
  i:Integer;

//--------- Globals -------------------------------
procedure XMLCreate;
begin
  fIXMLDocument := TXMLDocument.Create('.\try.xml');
  fIXMLDocument.Active;
end;

procedure XMLUse;
begin
  fIXMLDocument.DocumentElement;
end;

//------- TMyThread ------------------------------
procedure TMyThread.Execute;
begin
  FreeOnTerminate := TRUE;

  EnterCriticalSection(fCriticalSection);
  try
    CoinitializeEx(nil, COINIT_MULTITHREADED);
    try
      XMLUse;
    finally
      CoUninitialize;
    end;
  finally
    LeaveCriticalSection(fCriticalSection);
  end;
end;

//------------ Main -------------------------
begin
  InitializeCriticalSection(fCriticalSection);
  CoinitializeEx(nil, COINIT_MULTITHREADED);
  try
    XMLCreate;
    try
      for i := 0 to 100000 do
      begin
        TMyThread.Create(FALSE);
        sleep(100);
      end;
    finally
      fIXMLDocument := nil;
    end;
  finally
    CoUninitialize;
    DeleteCriticalSection(fCriticalSection);
  end;
end.

我正在使用Windows7上的Delphi7企业版。 非常欢迎任何帮助。


只需在第二个代码块中添加dfm,其余部分不需要。 - whosrdaddy
你的代码中有一个很大的问题:XML := TXMLDocument.Create('.\try.xml'); 应该是 fXMLDocument := TXMLDocument.Create('.\try.xml');,并且不需要 GetInterface 代码。 - whosrdaddy
第二个大红旗。你不能从线程访问GUI!!!在TMyThread.Execute中的Form1.XMLUse是不行的,因为XMLUse函数正在设置TEdit文本。我认为你需要正确掌握线程的基础知识。我建议阅读这篇关于Delphi多线程的优秀指南:http://web.archive.org/web/20060305174604/http://www.pergolesi.demon.co.uk/prog/threads/ToC.html - whosrdaddy
@David Heffman - 这是在一个已经晚期的项目中急需制作测试应用程序的最快方式。 :) 我以前从未在Delphi中制作过任何控制台应用程序 :( - peter
取决于您所说的“最快”。如果有一个SSCCE,我们可以直接编译、运行和立即检查。但现在情况不是这样的,我无法做到这一点。 - David Heffernan
显示剩余16条评论
3个回答

5

您正在使用自由线程模型。当您调用TXMLDocument.Create时,会创建一个单独的COM对象。之后,您在多个线程中使用该对象而没有进行任何同步。换句话说,您违反了COM线程规则。可能会存在更多问题,但在解决此问题之前不能继续执行。


我尝试过同步(不在测试应用程序中)-正如我所说,使用一个线程访问COM是可以的。我想作为最后的手段使用同步。据我所知,COM对象访问只能与apartment线程模型同步。使用free线程,您可能需要注意并发访问(使用critical sections等),但无需同步。我尝试了所有四种可能的'COINIT_`标志的替代方案,但仍然出现稳定的内存消耗。因此问题是:是否可以在任何可能的情况下不使用同步来使用它? - peter
2
什么是同步?使用互斥锁进行序列化可以解决问题。你的代码忽略了线程规则。为什么不同的线程需要共享对象?使用STA更简单。 - David Heffernan
不同的线程都只请求相同的接口,而且它们必须这样做。我的真实项目是一个高度多线程的项目。只要我没有被迫,我会尽量坚持这一点。 - peter
你对同步的定义和我的不一样。看这里:http://en.wikipedia.org/wiki/Synchronization_(computer_science) 我也不知道睡眠如何涉及其中。那不是同步工具。它不会改变你的线程模型,也不会消除存在的竞争。如果性能是一个问题,那么数据共享肯定不会有所帮助。隔离才是你需要的。使用STA以获得最佳性能。 - David Heffernan
你认为我们为什么会忘记真正的问题?线程不正确可能会导致内存泄漏。你明白为什么在每个线程中使用STA会给你带来更好的性能吗? - David Heffernan
显示剩余7条评论

0

这不是解决此问题的真正方法,但我通过在主线程上初始化一个IXMLDocument实例并将其引用传递给新创建的动态线程,在调用resume之前解决了它。采用这种方法,所有IXMLDocument的引用都保留在主线程上,因此当引用计数降至零时,Delphi可以处理它们。


0
这个问题没有得到解答,问题依旧未被解决。但我不得不自己解决它,最终,我决定切换到另一个XML实现。我的选择是OmniXML,内存消耗问题现在已经消失了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接