【问题标题】:Running VCL in a separate thread在单独的线程中运行 VCL
【发布时间】:2013-09-26 17:51:54
【问题描述】:

我现在遇到了一种相当罕见的情况。 我有一个直接与 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) 将向哪个线程发送消息?

Inb4“发给 VCL 和主应用程序的消息会很混乱”——它们不会,因为每个线程都有自己的消息队列。

另外,您可能会看到来源here。 (也许)有问题的库被命名为LuaDebugger。代替一个合适的客户端(CoreEngineClient)我目前正在使用LuaDefaultHost,这是一个相当简单的控制台应用程序,调用调试器并且行为主要类似于lua.exe。 使用控制台客户端,调试器的工作出奇地顺利——我遇到的唯一问题是,如果我在仍然使用库时关闭控制台窗口,VCL 会抛出“窗口处理程序不再有效”(俄语:/)。如果我让客户端按照预期的方式完成与调试器的交互,那么一切都会顺利。可能在单元定稿期间调用Windows.TerminateThread 应该可以解决这个问题。

【问题讨论】:

  • 要么我遗漏了一些东西,要么您正在访问和运行方法并在不同线程中创建的 Application 对象上运行消息队列。我认为它应该早点崩溃..
  • @SertacAkyuz 我也是这么想的。显然,VCL 比我们想象的更灵活。 :O 不过,我仍然没有尝试将库与 GUI 客户端一起使用,仅使用控制台客户端。可能我需要运行一个特殊的实验来确定 VCL 是否仍然以某种方式使用主线程的队列。
  • @Deligamer VCL 怎么能做到这一点? VCL 如何强制自己进入可执行文件拥有的线程?它不能通过武力做到这一点。它需要可执行文件的合作。 VCL 没有任何进程主线程的概念。只有 VCL 线程,即初始化 VCL 的线程。
  • @DavidHeffernan Units 的 initialization 代码在客户端线程中运行,Application.* 方法在调试器线程中运行。我假设单元初始化创建的资源未绑定到特定线程。看起来是这样,但我可能错了。之前你说过只有当我从一个新线程中调用LoadLibrary 时它才会起作用;现在你说移动Application.Initialize 是可以的。这是什么意思?
  • 我并没有说什么不同。初始化代码从 DllMain 运行。这决定了 VCL 线程。这就是调用 LoadLibrary 的线程,因为 DllMain 在调用 LoadLibrary 的线程中运行。因此,VCL 使用的线程是调用主机中的 LoadLibrary 的线程。你无法改变它。

标签: multithreading delphi c++builder vcl


【解决方案1】:

您唯一的希望是创建线程,然后从该线程加载 DLL。因此,为了尽可能清楚,您创建线程,然后从该线程中执行的代码调用LoadLibrary 来加载 DLL。

VCL 必须用完加载 DLL 的线程。 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

【讨论】:

  • 如果需要更详细的说明,只要你写了Application:=..(使用“表单”),你就已经有了一个应用程序实例。在uses 子句中包含'forms',拉取'graphics','controls'等。它们在它们的初始化部分设置VCL,在Dll 启动时调用。请注意,VCL Forms 应用程序的项目源中没有“Application:=”。
  • 我没有看到任何直接的问题。但是,在我面对它之前,我也不会想到模态问题..
  • 啊,我的错,这道题的引文是我凭记忆写的,写错了。实际内容从主 .dpr 文件剪切粘贴到 Execute。我现在就解决这个问题。
  • 我不太明白最后的评论。
  • 你不需要。我刚刚说我修正了一个错字。现在让我们关注线程问题。 :|
【解决方案2】:

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

所以,

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

似乎确实如此。从我们这里得到的信息来看,VCL 代码只是在“当前”线程中运行,并且不知道是否还有其他线程,或者这个“当前”线程是进程的主线程还是在同一个二进制文件中创建的。只要这个帖子不惹别人,一切都会好起来的。

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

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

【讨论】:

  • VCL 代码只在“当前”线程中运行。 那么你期待什么?!您调用Application.Run 并期望代码会神奇地迁移到其他线程!至于Synchronize,我看的代码很清楚。它在 VCL 线程(初始化模块的线程)上运行代码。那就是在System.MainThreadID中可以找到ID的线程。
  • 我投了反对票,因为您的答案明显不正确。 Synchronize 不会在进程主线程上执行代码。它在模块的初始化线程上执行它。我的答案的更新证明了这一点。
  • Synchronize() 不会发布到初始化模块的线程,它会发布到MainThreadID 当前指定的任何线程。它可以指定默认初始化模块的线程,但您可以将其更改为您想要的任何线程 ID。或者你甚至可以分配你自己的WakeMainThread 回调来做任何你想做的事情,它根本不需要发布。
【解决方案3】:

Remy Lebeau 来自 EDN 的回答:

DLL 有自己独立的 VCL 和 RTL 副本 从主应用程序的副本。大多数情况下,这种线程使用 DLL 里面一般是可以的,但是主线程敏感的功能, 像TThread.Synchronize()TThread.Queue(),除非你 使用您的 ThreadID 手动更新 System.MainThreadID 变量 “主”线程,除非您的线程定期调用CheckSynchronize()(其中 通常在TThread“唤醒”“主”线程时自动调用 当执行Synchronize/Queue 操作时)。

不知道手动调整System.MainThreadID是否安全,但这里主要问题的答案是“一般都可以”。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-08-12
    • 2012-05-20
    • 2015-01-24
    • 2011-04-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多