在单独的线程中运行VCL

5

我现在面临一个比较罕见的情况。我有一个应用程序直接与Windows消息队列交互。这个应用程序还运行带有LuaJIT的外部Lua脚本。我想为这些脚本创建一个调试工具,所以我创建了一个简单的VCL应用程序,然后将其转换成DLL库。当第一个应用程序使用该库开始调试会话时,该DLL会创建一个单独的线程,其中整个VCL设施被初始化并运行。

procedure TDebuggerThread.Execute;
begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm (TMainForm, MainForm);
  Application.Run;
end;

VCL是否完全支持以这种方式执行?TThread.Synchronize(Proc:TThreadProc)将把它的消息发送到哪个线程?
在“将消息发送到VCL和主应用程序可能会出错”的情况之前,它们不会出错,因为每个线程都有自己的消息队列。
另外,您可以在这里查看源代码here。可能存在问题的库名为LuaDebugger。目前我正在使用LuaDefaultHost替代适当的客户端(Core,Engine,Client),它是一个相当简单的控制台应用程序,调用调试器并且基本上像lua.exe一样运行。 使用控制台客户端时,调试器工作得非常顺畅——我遇到的唯一问题是,如果在仍在使用库的情况下关闭控制台窗口,则VCL将抛出“窗口句柄无效”(用俄语:/)。如果我让客户端按照预期与调试器互动完成,一切都很好。可能在最终化过程中调用Windows.TerminateThread可以解决这个问题。

@Delfigamer VCL怎么做到的?VCL怎么能强制进入可执行文件所拥有的线程?它不能通过强制来实现。它需要可执行文件的合作。VCL没有任何关于进程主线程的概念。只有初始化VCL的那个线程,也就是VCL线程。 - David Heffernan
@DavidHeffernan Units的初始化代码在客户端线程中运行,Application.*方法在调试器线程中运行。我假设由单元初始化创建的资源不绑定到特定线程。看起来是这样,但我可能错了。之前你说只有在我从新线程调用LoadLibrary时才能工作;现在你说移动Application.Initialize是可以的。这是什么意思? - Delfigamer
我并没有说什么不同的话。初始化代码是从DllMain运行的。这决定了VCL线程。这就是调用LoadLibrary的线程,因为DllMain在调用LoadLibrary的线程中运行。因此,VCL使用的线程是在主机中调用LoadLibrary的线程。你无法改变这一点。 - David Heffernan
你谈论客户端线程和调试器线程,这些是你的细节。我告诉你的是,每次访问 VCL 对象都必须在 VCL 线程中进行。 - David Heffernan
回顾您早期的评论之一,您说:<i>单元的初始化代码在客户端线程中运行,Application.*方法在调试器的线程中运行。</i>这不是VCL的规则,这可能是您描述应用程序如何工作的方式。如果客户端线程和调试器线程不同,则您所描述的内容将无法正常工作。正如我已经多次提到的,VCL访问必须在VCL线程上进行。而VCL线程是调用LoadLibrary的线程。因此,单元初始化(DllMain的一部分)和Application.*方法必须在同一线程上运行。 - David Heffernan
显示剩余4条评论
3个回答

7
您唯一的希望是创建线程,然后从该线程内部执行的代码调用LoadLibrary来加载DLL。必须在加载DLL的线程中运行VCL。VCL初始化发生在DLL的初始化期间,并确定哪个线程是VCL主线程。 VCL主线程是初始化VCL的线程,即加载DLL的线程。您可能需要保持头脑清醒,因为此方法涉及在单个进程中拥有两个GUI线程和两个消息泵。显示模态窗口会禁用两个GUI线程上的窗口。我无法确定这种方法(同一进程中有两个GUI线程之一是VCL线程)是否有效,因为我从未这样做过。但我认为成功的机会很大。
您还提出了一个非常具体的问题:
TThread.Synchronize(Proc:TThreadProc)将其消息发送到哪个线程?
答案总是初始化模块的线程。对于可执行文件,这是进程的主线程。对于DLL,初始化模块的线程是调用LoadLibrary的线程,执行DllMain的初始调用的线程,执行DLL单元的初始化代码的线程。在RTL / VCL中,这称为模块的主线程。它的ID由System.MainThreadID给出。为了证明这一点,请看下面的演示。
可执行文件
program DllThreading;

{$APPTYPE CONSOLE}

uses
  Classes, Windows;

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

procedure TMyThread.Execute;
var
  lib: HMODULE;
  proc: procedure; stdcall;
begin
  lib := LoadLibrary('dll.dll');
  proc := GetProcAddress(lib, 'foo');
  proc();
  Sleep(INFINITE);
end;

begin
  Writeln('This is the process main thread: ', GetCurrentThreadId);
  TMyThread.Create;
  Readln;
end.

DLL

library Dll;

uses
  Classes, Windows;

type
  TMyThread = class(TThread)
  private
    procedure DoStuff;
  protected
    procedure Execute; override;
  end;

procedure TMyThread.DoStuff;
begin
  Writeln('This is the thread which executes synchronized methods in the DLL: ', GetCurrentThreadId);
end;

procedure TMyThread.Execute;
begin
  Writeln('This is the thread created in the DLL: ', GetCurrentThreadId);
  Synchronize(DoStuff);
end;

procedure foo; stdcall;
begin
  TMyThread.Create;
  CheckSynchronize(1000);
end;

exports
  foo;

begin
  Writeln('This is the initialization thread of the DLL: ', GetCurrentThreadId);
end.

输出

这是主线程的进程:2788
这是DLL的初始化线程:5752
这是在DLL中创建的线程:6232
这是在DLL中执行同步方法的线程:5752

我真的不太理解那个最后的评论。 - David Heffernan
你不需要这么做。我刚才说过我已经修复了一个打字错误。现在让我们专注于线程问题。 :| - Delfigamer
好的。我坚持我所说的话。 - David Heffernan
现在,在你提出的问题的评论中,你问我是否知道我在说什么。你发表了一个回答,我证明是不正确的。我确实知道我在说什么。我认为你需要放慢一点速度,试着理解我在说什么。也许我没有清楚地表达自己。但我相信我们可以解决这个问题,使我们都在同一频率上。 - David Heffernan
嗯,让我们向一些Embarcadero论坛的领袖提问吧(https://forums.embarcadero.com/thread.jspa?threadID=93327) - Delfigamer
显示剩余9条评论

1

EDN的Remy Lebeau回答如下:

动态链接库(DLL)有自己独立的VCL和RTL拷贝,与主应用程序的拷贝分开。在大多数情况下,这种线程内使用DLL的方式通常是可以的,但是主线程敏感的功能,如TThread.Synchronize()TThread.Queue(),除非您手动更新System.MainThreadID变量为您的“主”线程的ThreadID,否则将无法工作,除非您的线程定期调用CheckSynchronize()(当执行Synchronize/Queue操作时,TThread会自动唤醒“主”线程)。

不知道手动调整System.MainThreadID是否安全,但是对于主要问题的答案是“通常可以的”。


-1

哦,太酷了,我正在回答自己的问题。

所以,

VCL是否完全支持以这种方式执行?

看起来是的。从我们这里得到的信息来看,VCL代码只在“当前”线程中运行,并且不知道是否有其他线程或者这个“当前”线程是进程的主线程还是在同一个二进制文件中创建的。只要这个线程不干扰其他线程,一切都很好。

TThread.Synchronize(Proc:TThreadProc)将消息发送到哪个线程?

实验表明,该消息将被发送到进程的主线程,而不是VCL线程(自然而然地,它是Application.Run工作的线程)。要与VCL线程同步,我必须显式地向VCL表单发送消息,在这里(LuaDebugger/USyncForm.pas),它是带有WParam指向同步代码的指针和LParam可选的void*参数的自定义UM_MethodCall。


VCL代码只在“当前”线程中运行。你期望什么?!你调用Application.Run,期望代码会神奇地迁移到其他线程!至于Synchronize,我正在查看的代码非常清晰。它在VCL线程上运行代码,也就是初始化模块的线程。这个线程的ID可以在System.MainThreadID中找到。 - David Heffernan
我进行了投票否决,因为你的答案明显是错误的。Synchronize不会在进程的主线程上执行代码,而是在模块初始化线程上执行。我回答中的更新证明了这一点。 - David Heffernan
1
Synchronize() 不会发布到初始化模块的线程,而是发布到当前由 MainThreadID 指定的任何线程。它可能默认指定了初始化模块的线程,但您可以将其更改为任何想要的线程 ID。或者,您甚至可以分配自己的 WakeMainThread 回调来执行任何操作,它不必发布任何内容。 - Remy Lebeau

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