为什么我不需要在一个在COM线程内创建的线程中调用CoInitialize?

3
为了学习多线程,我在COM线程(TRemoteDataModule)中创建了一个线程。
这是我的组件工厂:
TComponentFactory.Create(ComServer, TServerConn2, Class_ServerConn2, ciMultiInstance, tmApartment);

在线程内部,我不需要调用CoInitialize来使用TADOQuery.Create、.Open和.Exec。我阅读到需要在调用库函数之前在线程上初始化COM库,除了CoGetMalloc函数外,以获取指向标准分配器和内存分配函数的指针。但是,在这种情况下,缺少CoInitialize并没有给我带来任何麻烦。
这是否与线程模型有关?我在哪里可以找到关于这个主题的解释?
更新:当我说“内部”,它意味着在COM方法上下文内部。
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;

更新 2:

用于连接数据库的TADOConnection当前在COM线程上下文中创建(TThread.Create构造函数)。 尽管如此,TADOConnection.OpenTADOQuery.Create/.Open都是在TThread.Execute内执行的。

更新 3 - 模拟

接口:

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;

实现:

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.

更新 4

错误应该发生在这里:

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;

一些原因(可能是由于 TComponentFactory ?)使得 CoCreateInstance 确认 TWorker 与 TServerConn2 在同一上下文中,因此不会引发错误?

也许,在执行您的线程过程之前,Delphi会在新线程上为您调用CoInitialize - noseratio - open to work
我不知道,我认为这与线程模型有关... - EProgrammerNotFound
@Noseratio,与.Net不同,Delphi对我来说不会调用CoInitialize - EProgrammerNotFound
我不是Delphi专家,但我对COM线程模型有一定的了解。您使用tmApartment指定线程安全的类工厂,但它创建的COM对象实例不是线程安全的。如果您手动创建一个线程,确实应该在其上调用CoInitialize,并且从该线程中不应直接调用在主线程或其他线程上创建的任何对象。您可以使用COM封送来解决这个问题。 - noseratio - open to work
好的,你已经添加了TWorker,但它的Execute方法在哪里?你使用TWorkerTread.Create(self)创建了它 - 这甚至无法编译(线程不是组件,它的可选参数是布尔值 - CreateSuspended)。你还使用了完全不同的类名;TWorker怎么变成了TWorkerTread?一个TApartmentThread将以IClassFactory作为构造函数参数,并且也可以分配给TThread变量 - 这只是让我们回到我的答案,即你正在使用TApartmentThread - J...
显示剩余2条评论
3个回答

5

下列可能适用于一个或多个:

  1. 在未使用COM初始化的线程中,所有现有的接口指针将一直工作,直到您进行COM API调用或需要进行COM编组(然后检测出未初始化的线程)。也就是说,您的“没有给我带来麻烦”实际上可能为时过早。

  2. 如果进程中的任何线程使用COINIT_MULTI­THREADED标志调用Co­Initialize­[Ex],那么不仅将当前线程初始化为多线程公寓的成员,而且还表示,“从未调用Co­Initialize­[Ex]的任何线程也是多线程公寓的一部分。” - 所谓的隐式MTA事物


1
使用 tmApartment 创建 TComponentFactory 会导致调用 COINIT_APARTMENTTHREADEDCoInitialize,我非常确定。你需要为 COINIT_MULTITHREADED 指定 tmFreetmBoth - J...
刚刚几分钟前还能工作,我猜是间歇性的问题。 - Roman R.
Update3中的代码仅用于复现,请不要关心线程安全。 - EProgrammerNotFound
@MatheusFreitas - 我并不完全相信... 你确定你没有抑制调试器异常,线程只是默默地失败了吗?你确认查询实际上正在运行并获取数据了吗? - J...
@J... 我很确定!请将update3中的代码复制并在Delphi 6中运行,你会看到结果的。 - EProgrammerNotFound

4
当前在 COM 线程上下文(TThread.Create 构造函数)创建了用于连接到数据库的 TADOConnection。尽管 TADOConnection.Open 和 TADOQuery.Create/.Open 都是在 TThread.Execute 内执行的。
这样做出现两个问题:
1. TWorker.Create() 和 TWorker.Execute() 将在不同的线程上下文中运行。Create()将在调用TServerConn2.CheckException()的线程的上下文中运行(此前已经调用了CoInitialize/Ex()),但Execute()将在TThread线程的上下文中运行。 ADO 是基于单元线程的,这意味着其 COM 接口不能跨线程/单元线程边界使用,除非通过 IGlobalInterfaceTable 接口或 CoMarshalInterThreadInterfaceInStream() 和 CoGetInterfaceAndReleaseStream() 函数进行编组。
2. 即使你对 ADO 接口进行了编组,TWorker.Execute() 也必须在自身上调用 CoInitialize/Ex()。每个单独的线程都必须初始化 COM 以建立其线程模型,然后才能访问任何 COM 接口。线程模型决定了 COM 如何访问接口(直接还是通过代理),是否使用消息队列等。
因此,解决问题的简单方法是根本不要在线程边界上创建和使用 ADO 组件。将 TADOConnection 移入 Execute() 中即可。
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;

我认为这与@Roman的回答有很大关系,你可以在这里看到链接 - EProgrammerNotFound
1
据我所知,ADO对象在没有进行封送的情况下不能跨线程边界使用。然而,话虽如此,我必须同意@RomanR的观点,即TADOQuery没有关于未调用CoInitialize()的异常,因为TThread线程可能会继承隐式MTA(这是我今天之前从未听说过的)。在COM编程中的经验法则是,在任何线程中使用任何COM对象之前,始终在自己的代码中显式调用CoInitialize/Ex()。这样就不会有像这样的意外情况发生。 - Remy Lebeau
ADO对象在没有进行封送处理的情况下无法跨线程边界使用。请问我可以在哪里阅读到这个规则? - EProgrammerNotFound
1
@MatheusFreitas:这不是ADO特定的规则,而是适用于公寓线程模型(与多线程和自由线程模型相对)的一般规则。公寓线程模型 COM对象只能在创建它们的同一线程上下文中使用。为了跨线程边界使用此类对象,必须进行编组,以便COM可以创建代理将对象调用委派给实际运行对象的原始线程(这需要原始线程具有消息循环)。据我所知,ADO使用公寓线程模型 COM对象。 - Remy Lebeau
2
@MatheusFreitas:阅读MSDN,例如:"进程、线程和公寓""单线程公寓",以及"在不同公寓中访问接口" - Remy Lebeau
显示剩余3条评论

1
使用TComponentFactory创建公寓线程时,它会为您调用CoInitializeCoUninitialize - 这就在VCL源代码中(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;

工作线程继承了 TThread 而不是 TApartmentThread。此外,我正在使用 Delphi 6。 - EProgrammerNotFound
@MatheusFreitas,请展示你用于创建线程的确切代码。你展示了使用TComponentFactory - 如果你正在使用它创建线程,那么这就是它们来自的地方(TComponentFactory.CreateInstance)。 - J...
TComponentFactory被放置在Delphi IDE的初始化部分中initialization TComponentFactory.Create(ComServer, TServerConn2, Class_ServerConn2, ciMultiInstance, tmApartment);当您选择接口名称时。 - EProgrammerNotFound
Update3中的代码仅用于复现,请不要关心线程安全。 - EProgrammerNotFound

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