3

In order to learn multithreading, I've created a thread inside a COM Thread (TRemoteDataModule).

This is my Component Factory:

TComponentFactory.Create(ComServer, TServerConn2, Class_ServerConn2, ciMultiInstance, tmApartment);

Inside the Thread, I didn't needed to Call CoInitialize to use TADOQuery.Create, .Open... .Exec

I read that I need to initialize the COM library on a thread before you call any of the library functions except CoGetMalloc, to get a pointer to the standard allocator, and the memory allocation functions.

But in this case, the absence of CoInitialize didn't brought me any trouble.
Is this related with Thread Model? Where can I Find the explanation for this subject?

UPDATE:

When I say INSIDE, it means inside the COM method context:

interface
type
  TWorker = class(TThread); 

  TServerConn2 = class(TRemoteDataModule, IServerConn2)
  public 
    procedure Method(); safecall;
  end;


implementation 
  procedure TServerConn2.Method(); 
  var W: TWorker;
  begin
    W := TWorkerTread.Create(Self);
  end;

UPDATE 2:

The TADOConnection used to connect to database are currently being created in the COM Thread context (TThread.Create constructor). Although, TADOConnection.Open and TADOQuery.Create/.Open are both being performed inside TThread.Execute .

UPDATE 3 - Simulacrum

Interface:

type
  TServerConn2 = class;

  TWorker = class(TThread)
  private
    FDB: TADOConnection;
    FOwner: TServerConn2;
  protected
    procedure Execute; override;
  public
    constructor Create(Owner: TServerConn2);
    destructor Destroy; override;
  end;

  TServerConn2 = class(TRemoteDataModule, IServerConn2)
    ADOConnection1: TADOConnection;
    procedure RemoteDataModuleCreate(Sender: TObject);
  private
    { Private declarations }
  protected
    class procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); override;
    procedure CheckException; safecall;
  public
    User, Pswd, Str: String;
    Ok: Boolean;
  end;

Implementation:

class procedure TServerConn2.UpdateRegistry(Register: Boolean; const ClassID, ProgID: string);
begin
  if Register then
  begin
    inherited UpdateRegistry(Register, ClassID, ProgID);
    EnableSocketTransport(ClassID);
    EnableWebTransport(ClassID);
  end else
  begin
    DisableSocketTransport(ClassID);
    DisableWebTransport(ClassID);
    inherited UpdateRegistry(Register, ClassID, ProgID);
  end;
end;

{ TWorker }

constructor TWorker.Create(Owner: TServerConn2);
begin
  inherited Create(False);
  FreeOnTerminate := True;
  FDB := TADOConnection.Create(nil);
  FOwner := Owner;
end;

destructor TWorker.Destroy;
begin
  FDB.Free;
  FOwner.Ok := True;
  inherited;
end;

procedure TWorker.Execute;
var Qry: TADOQuery;
begin
  FDB.LoginPrompt := False;
  FDB.ConnectionString := FOwner.Str;
  FDB.Open(FOwner.User, FOwner.Pswd);

  Qry := TADOQuery.Create(nil);
  try
    Qry.Connection := FDB;
    Qry.LockType := ltReadOnly;
    Qry.SQL.Text := 'SELECT TOP 1 * FROM MyTable';
    Qry.Open;
  finally
    Qry.Free;
  end;
end;

procedure TServerConn2.CheckException;
var W: TWorker;
begin
  W := TWorker.Create(Self);
  while not Ok do Sleep(100);
end;

procedure TServerConn2.RemoteDataModuleCreate(Sender: TObject);
begin
  User := 'user';
  Pswd := 'pass';
  Str := ADOConnection1.ConnectionString;
end;

initialization
  TComponentFactory.Create(ComServer, TServerConn2,
    Class_ServerConn2, ciMultiInstance, tmApartment);
end.

UPDATE 4

The error should happen here:

function CreateADOObject(const ClassID: TGUID): IUnknown;
var
  Status: HResult;
  FPUControlWord: Word;
begin
  asm
    FNSTCW  FPUControlWord
  end;
  Status := CoCreateInstance(ClassID, nil, CLSCTX_INPROC_SERVER or
    CLSCTX_LOCAL_SERVER, IUnknown, Result);
  asm
    FNCLEX
    FLDCW FPUControlWord
  end;
  if (Status = REGDB_E_CLASSNOTREG) then
    raise Exception.CreateRes(@SADOCreateError) else
    OleCheck(Status);
end;

By somehow (because of TComponentFactory maybe?) CoCreateInstance identifies that TWorker is in the same context than TServerConn2 and don't raise errors?

EProgrammerNotFound
  • 2,403
  • 4
  • 28
  • 59
  • Perhaps, Delphi calls `CoInitialize` for you on the new thread before executing your thread procedure? – noseratio Aug 09 '13 at 12:56
  • I don't know, I think there is something related with Thread Model... – EProgrammerNotFound Aug 09 '13 at 13:00
  • @Noseratio unlike .Net, Delphi doesn't call `CoInitialize` to me! – EProgrammerNotFound Aug 09 '13 at 13:10
  • I'm not am expert in Delphi but I'm a bit familiar with [COM threading models](http://support.microsoft.com/kb/150777). You specify yours with `tmApartment`, which means the class factory is thread-safe, but the instances of the COM objects it creates are not thread-safe. If you manually create a thread, you really should call `CoInitialize` on it, and from that thread you should not directly call any objects, created on the main thread or other thread. You could use [COM marshalling](http://msdn.microsoft.com/en-us/library/windows/desktop/ms693719(v=vs.85).aspx) for that. – noseratio Aug 09 '13 at 13:20
  • @Noseratio `TADOQuery` raises an `Exception` when `CoInitialize` has not been called in the thread context in which it has been created, the strange behavior in this situation is that: it is not raising the `Exception` at all. Also, it is working without `CoInitialize` – EProgrammerNotFound Aug 09 '13 at 13:28
  • Ok, so you've added TWorker but where is its `Execute` method? You've created it using `TWorkerTread.Create(self)` - this won't even compile (a thread isn't a component, its optional argument is a boolean - `CreateSuspended`). You've also used a totally different class name; how did `TWorker` become `TWorkerTread`? A `TApartmentThread` will take an `IClassFactory` as a constructor argument and can also be assigned to a `TThread` variable - this just brings us back to my answer which is that you are using a `TApartmentThread` – J... Aug 09 '13 at 14:00
  • @J... The code is an example, an abstraction. The exact code is enormous. I can make a similar to reproduce but it will take some minutes. – EProgrammerNotFound Aug 09 '13 at 14:04

3 Answers3

5

Either or both of the following might apply:

  1. On a thread not initialized with COM all existing interface pointers keep working until you make a COM API call or otherwise require COM marshalling which then fails detecting an uninitialized thread. That is, your "didn't brought me any trouble" might actually be too early to say.

  2. If any thread in the process calls Co­Initialize­[Ex] with the COINIT_MULTI­THREADED flag, then that not only initializes the current thread as a member of the multi-threaded apartment, but it also says, "Any thread which has never called Co­Initialize­[Ex] is also part of the multi-threaded apartment." - so called impicit MTA thing

Roman R.
  • 68,205
  • 6
  • 94
  • 158
  • 1
    Creating a `TComponentFactory` with `tmApartment` results in CoInitialize being called with `COINIT_APARTMENTTHREADED`, I`m quite sure. You need to specify `tmFree` or `tmBoth` for `COINIT_MULTITHREADED`. – J... Aug 09 '13 at 14:29
  • Worked just a few minutes ago, I suppose it's intermittent. – Roman R. Aug 09 '13 at 14:31
  • The code in Update3 is only to reproduce, don't please don't care about thread-safety – EProgrammerNotFound Aug 09 '13 at 14:36
  • @MatheusFreitas - I'm not entirely convinced... are you sure you haven't suppressed the debugger exception and the thread isn't just failing silently? Have you confirmed that the query is actually running and fetching data? – J... Aug 09 '13 at 18:10
  • @J... I'm sure! Please, copy the code in update3, and run it in Delphi 6. You'll se for yourself. – EProgrammerNotFound Aug 09 '13 at 19:07
4

The TADOConnection used to connect to database are currently being created in the COM Thread context (TThread.Create constructor). Although, TADOConnection.Open and TADOQuery.Create/.Open are both being performed inside TThread.Execute .

That will not work, for 2 reasons:

  1. TWorker.Create() and TWorker.Execute() will run in different thread contexts. Create() will run in the context of the thread that is calling TServerConn2.CheckException() (which will have already called CoInitialize/Ex() on itself beforehand), but Execute() will run in the context of the TThread thread instead. ADO is apartment threaded, which means its COM interfaces cannot be used across thread/apartment boundaries unless you marshal them, either via the IGlobalInterfaceTable interface or the CoMarshalInterThreadInterfaceInStream() and CoGetInterfaceAndReleaseStream() functions.

  2. even if you did marshal the ADO interfaces, TWorker.Execute() must call CoInitialize/Ex() on itself. EVERY individual thread must initialize COM to establish its threading model before then accessing any COM interfaces. The threading model dictates how COM accesses interfaces (direct or through proxies), whether message queues are used, etc.

So the simple solution to your problem is to NOT create and use the ADO components across thread boundaries at all. Move your TADOConnection into Execute() instead:

constructor TWorker.Create(Owner: TServerConn2);
begin
  inherited Create(False);
  FreeOnTerminate := True;
  FOwner := Owner;
end;

destructor TWorker.Destroy;
begin
  FOwner.Ok := True;
  inherited;
end;

procedure TWorker.Execute;
var
  DB: TADOConnection;
  Qry: TADOQuery;
begin
  CoInitialize;
  try
    DB := TADOConnection.Create(nil);
    try
      DB.LoginPrompt := False;
      DB.ConnectionString := FOwner.Str;
      DB.Open(FOwner.User, FOwner.Pswd);

      Qry := TADOQuery.Create(nil);
      try
        Qry.Connection := DB;
        Qry.LockType := ltReadOnly;
        Qry.SQL.Text := 'SELECT TOP 1 * FROM MyTable';
        Qry.Open;
      finally
        Qry.Free;
      end;
    finally
      DB.Free;
    end;
  finally
    CoUninitialize;
  end;
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • I think it's really related to @Roman answear as you can see [here](http://blogs.msdn.com/b/oldnewthing/archive/2013/04/19/10412399.aspx) – EProgrammerNotFound Aug 09 '13 at 17:05
  • 1
    AFAIK, ADO object *cannot* be used across thread boundaries without marshaling. However, that being said, I would have to agree with @RomanR in that `TADOQuery` is not raising an exception about an uncalled `CoInitialize()` because the `TThread` thread is likely inheriting the **implicit MTA** (which I have never heard of before today). The rule of thumb in COM programming is to **ALWAYS** call `CoInitialize/Ex()` explicitly in your own code before using any COM objects in any thread. That way, there are no surprises like this. – Remy Lebeau Aug 09 '13 at 17:21
  • `ADO object cannot be used across thread boundaries without marshaling`. Where may I read this rule, please? – EProgrammerNotFound Aug 09 '13 at 17:37
  • @MatheusFreitas, you can always try but don't come running back crying. Remy knows what he's talking about. The culprit is that an ADOConnection can NOT be shared over multiple threads. Each thread must have it's own connection. I am sure this is documented somewhere, but we know this from experience :) – whosrdaddy Aug 09 '13 at 18:26
  • 1
    @MatheusFreitas: It is not a rule of ADO specifically, it is a rule for the *Apartment-Threaded* model in general (vs Multi-Threaded and Free-Threaded). *Apartment-Threaded* COM objects can only be used in the same thread context that they are created in. In order to use such an object across thread boundaries, it must be marshaled so COM can create a proxy to delegate object calls to the original thread that the object is actually running in (which requires the original thread to have a message loop). AFAIK, ADO uses *Apartment-Threaded* COM objects. – Remy Lebeau Aug 09 '13 at 18:45
  • @whosrdaddy I was not contesting what Remy said, I was just asking for documentation, but was just for future reference. – EProgrammerNotFound Aug 09 '13 at 18:56
  • Well, I don't know, what kind of troubles is expected in this situation, I guess never happened because the COM Thread is sleeping until the worker thread finishes – EProgrammerNotFound Aug 09 '13 at 19:13
  • 2
    @MatheusFreitas: Read MSDN, such as: ["Processes, Threads, and Apartments"](http://msdn.microsoft.com/en-us/library/windows/desktop/ms693344.aspx), ["Single-Threaded Apartments"](http://msdn.microsoft.com/en-us/library/windows/desktop/ms680112.aspx), and ["Accessing Interfaces Across Apartments"](http://msdn.microsoft.com/en-us/library/windows/desktop/ms682353.aspx). – Remy Lebeau Aug 09 '13 at 19:19
1

When you create an apartment thread using TComponentFactory it calls CoInitialize and CoUnInitialize for you - it's right in the VCL source (System.Win.VCLCom.pas):

procedure TApartmentThread.Execute;
var
  msg: TMsg;
  Unk: IUnknown;
begin
  try
    CoInitialize(nil);  // *** HERE
    try
      FCreateResult := FFactory.CreateInstanceLic(FUnkOuter, nil, FIID, '', Unk);
      FUnkOuter := nil;
      FFactory := nil;
      if FCreateResult = S_OK then
        CoMarshalInterThreadInterfaceInStream(FIID, Unk, IStream(FStream));
      ReleaseSemaphore(FSemaphore, 1, nil);
      if FCreateResult = S_OK then
        while GetMessage(msg, 0, 0, 0) do
        begin
          DispatchMessage(msg);
          Unk._AddRef;
          if Unk._Release = 1 then break;
        end;
    finally
      Unk := nil;
      CoUninitialize;  // ** AND HERE
    end;
  except
    { No exceptions should go unhandled }
  end;
end;
J...
  • 30,968
  • 6
  • 66
  • 143
  • The worker thread inherit's `TThread` instead of `TApartmentThread`. Also, I'm using Delphi 6 – EProgrammerNotFound Aug 09 '13 at 13:47
  • @MatheusFreitas then please show the exact code you are using to create the thread. You showed using `TComponentFactory` - if you're creating threads with it, this is where they come from (`TComponentFactory.CreateInstance`) – J... Aug 09 '13 at 13:49
  • TComponentFactory is place in the initialization section by Delphi IDE `initialization TComponentFactory.Create(ComServer, TServerConn2, Class_ServerConn2, ciMultiInstance, tmApartment);` When you choose the interface name. – EProgrammerNotFound Aug 09 '13 at 13:52
  • The code in Update3 is only to reproduce, don't please don't care about thread-safety – EProgrammerNotFound Aug 09 '13 at 14:35