5

It seems that there is some memory problem in Delphi7 when accessing COM object interfaces such as IXMLDocument and IXMLNode - and so forth - in a multithreaded way. Other COM interfaces may share this problem, but my "research" isn't that deep cause I have to proceed my current project as well. Creating TXMLDocument and manipulating it via interfaces like IXMLDocument and IXMLNode on a single thread is ok, but in a multithreading approach, when one thread creates the TXMLDocument object and the others manipulates it uses more and more memory. CoInitializeEx(nil, COINIT_MULTITHREADED) is called in every thread but in vain. It seems that every thread allocates some memory when getting an interface and does not free it, but every thread allocates it once - at least for a certain interface - e.g. the DocumentElement or ChildNodes - so one working thread beside the one that created the object - does not cause visible memory leak. But dynamically created threads are all behave the same way and eventually consume up process memory.

Here is my full test application Delphi7 form as SCCE which try to show three different scenario mentioned above - single thread, one working thread and dynamically created threads.

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.

Well, it is more than necessary cause it's a working Delphi form with buttons and timers and less because you cannot just copy and compile it. Here is the form's dfm as well:

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

And here is a console app. Just run it and see if any memory consumption occurs. Modify it as you like if you think it can be written a way that preserve multithreading but does not eat up memory:

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.

I'm using Delphi7 Enterprise on Windows7. Any help is very welcomed.

peter
  • 51
  • 4
  • just add the dfm in a second code block, the rest is not needed – whosrdaddy Oct 30 '13 at 06:59
  • One big red flag in your code: `XML := TXMLDocument.Create('.\try.xml');`. should be `fXMLDocument := TXMLDocument.Create('.\try.xml');`. and get rid of the GetInterface code. – whosrdaddy Oct 30 '13 at 07:07
  • Second big red flag. You can't access the GUI from a thread!!! `Form1.XMLUse` in `TMyThread.Execute` is a no go because the XMLUse function is setting TEdit text. It seems to me you need to get the basics of threading correct. I suggest reading this excellent guide to Delphi multithreading: http://web.archive.org/web/20060305174604/http://www.pergolesi.demon.co.uk/prog/threads/ToC.html – whosrdaddy Oct 30 '13 at 07:12
  • I don't have time at the moment but I will provide you sample of a correct usage of XMLDocument in threads. – whosrdaddy Oct 30 '13 at 07:15
  • We don't need GUI. Just make an SSCCE in a console app. – David Heffernan Oct 30 '13 at 07:23
  • @whosrdaddy That's sounds great. Thanks. See any chance that it would solve the problem as well? – peter Oct 30 '13 at 07:25
  • @David Heffman - That was the fastest way to produce a test application in a hurry of an already late project. :) I've never made any console app before in Delphi :( – peter Oct 30 '13 at 07:28
  • Depends on what you mean by fastest. With an SSCCE, we could just compile and run and inspect immediately. As it stands, I cannot do that. – David Heffernan Oct 30 '13 at 07:41
  • @TLama You mean you don't know how to make a sleep based timer in a thread? The skill of debugging is all about isolation. – David Heffernan Oct 30 '13 at 08:07
  • @whosrdaddy - well you can just omit `TEdit.text` assigment - that's not an important part - and get rid of the `Getinterface` code as you pointed out but the memory consumption remains steady only calling fIXMLDocument.DocumentElement. That's what really concerns. – peter Oct 30 '13 at 08:10
  • @peter I would have looked at this by now if I could have run it easily. As it is, there's too big a hurdle in the way. Time is short you know. – David Heffernan Oct 30 '13 at 08:44
  • So, what is `fIXMLDocument`? – David Heffernan Oct 30 '13 at 09:01
  • @DavidHeffernan, its a IXMLDocument interface? – whosrdaddy Oct 30 '13 at 09:42
  • @whosrdaddy It may well be, it's just not declared anywhere. – David Heffernan Oct 30 '13 at 09:45
  • @DavidHeffernan: its int the code, form1 private section `fXMLDocument:IXMLDocument;` – whosrdaddy Oct 30 '13 at 09:48
  • @whosrdaddy Nope. That's `fXMLDocument`. I'm talking about `fIXMLDocument`. – David Heffernan Oct 30 '13 at 09:50
  • I think that's a typo :) – whosrdaddy Oct 30 '13 at 10:08
  • @whosrdaddy I don't see how you can make a typo when pasting code that is running. My point is that this is a poor attempt at an SSCCE. Details like this really matter. – David Heffernan Oct 30 '13 at 10:23
  • @David fXMLDocument - as it is declared in the code - is an IXMLDocument interface. Sorry my talking about fIXMLDocument. It is a loos nomenclature, sorry about that. (My excuse is that it was meant to be fIXMLDocument. But the type of it what really matters.) – peter Oct 30 '13 at 13:23
  • What really matters is that the code doesn't compile. That's my point. You may think I'm being annoyingly pedantic, but I believe this stuff really matters. – David Heffernan Oct 30 '13 at 13:32
  • @David - No, I dont thing that. I added a console app first of all for you :) And thanks for your care. – peter Oct 30 '13 at 16:50

3 Answers3

5

You are using the free-threaded threading model. You create a single COM object when you call TXMLDocument.Create. You then use that object from multiple threads without any synchronization. In other words, you are contravening the COM threading rules. There may be more problems than this, but you cannot expect to proceed until you deal with this.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • I tried with synchronization (not in test app) - as I stated that using one thread to access `COM` is ok. I want to use sinch as the last resort. As far as I know `COM` objects access have only be synchronized with `apartment` threaded model. Using `free` threading you may have to watch for concurrent access (using `critical sections`, etc.) but no need of synch. I tried all four possible alternatives of 'COINIT_` flag in respect of the creator and working thread. Just steady memory consumption. So the questin is: can it be used in any possible way without synchronization? – peter Oct 30 '13 at 13:15
  • 2
    Well, what is synchronization? Using a mutex to serialize would do the trick. Your code ignores threading rules. Why do the different threads need to share objects? Simplre to use sta. – David Heffernan Oct 30 '13 at 13:21
  • Under synchronization I mean forcing a thread to pass parameters and function pointer to another thread (e.g. the main thread) that would call that function while the first thread in effective wait loop. In this way we can perform part of a code just as if in single threaded mode. Other synch tools like critical sections force the thread to wait but perform the code on the original thread. In this test app there is no need of use these other tools because of the sleep. – peter Oct 30 '13 at 14:14
  • Different threads all ask for only the same interface and they have to. My real project is a highly multithreading one. As long as I am not forced, I'll try to stick to this. – peter Oct 30 '13 at 14:15
  • Your definition of synchronization is not the same as mine. Look here: http://en.wikipedia.org/wiki/Synchronization_(computer_science) I also don't know where sleep comes into it. That's not a synchronisation tool. That doesn't change your threading model and somehow remove the race that exists. If performance is an issue then data sharing is surely not going to help. Compartmentalising is what you need. Use STA for best performance. – David Heffernan Oct 30 '13 at 14:17
  • The greater the sleep the bigger the chance that no concurrent calling of `Execute` occurs.:) (But that's not for production code of course). Well, I think you guys all forget the real question: "memory leak on multithreading" while talking about all this interesting thread business... Anyway I'm going to add a console app shortly. – peter Oct 30 '13 at 16:35
  • Why do you think we forget the real question? Incorrect threading could very well lead to memory leak. Do you understand why using an STA with one document per thread will give you better performance? – David Heffernan Oct 30 '13 at 16:36
  • @David - See here: I run a thread that reads messages from database one by one. That thread starts a working thread which builds up a BASE24 message based on information red from XML and then passes it to TCP/IP. Meanwhile the first thread may or may not read another message. If so it starts another working thread that will access XML too. So it is multithreading. That's the concept. You want me to shrink this into a single thread approach just because we don't know why it consumes up memory? You are saying that there is no way to do this nicely multithreading? – peter Oct 30 '13 at 16:59
  • I did not say single thread. I said STA, single thread apartment. But you can have many of those in a process. But sharing is always what hinders scaling. Try to avoid it. – David Heffernan Oct 30 '13 at 17:08
  • If I change `Coinitialize`'s second parameter to `COINIT_APARTMENTTHREADED` nothing changes in the behavior of the program. :( And as i said I tried all possible variations. Can you show me on the console app what am I to really do? :) – peter Oct 30 '13 at 17:32
  • Well no. In an STA you cannot share objects between apartments. – David Heffernan Oct 30 '13 at 17:54
  • Yes, it is said to be, but as far as I tried there is no real differences in program behavior. Anyway, thanks for your help. – peter Oct 30 '13 at 18:16
0

The question is not answered, the problem remained unsolved. But I had to solve it for myself so eventually I decided to switch to another XML implementation. My choose was OmniXML and memory consumption now disappeared.

peter
  • 51
  • 4
0

This is not a real solution for this issue, but I got through it initiating an IXMLDocument instance on main thread and passing it reference to the new created dynamic thread before call resume. With this approach all references of IXMLDocument remain on mainthread, so the Delphi can handle then all when referencecount goes to zero.

RBT
  • 24,161
  • 21
  • 159
  • 240