【问题标题】:How could a Delphi 6 TWinControl descendant's WndProc() execute sometimes off the main VCL thread?Delphi 6 TWinControl 后代的 WndProc() 有时如何在主 VCL 线程之外执行?
【发布时间】:2012-02-22 00:30:37
【问题描述】:

我有一个多线程的 Delphi 6 应用程序。我创建了一个源自 TWinControl 的组件。当我第一次构建它时,我使用了一个隐藏窗口,它是 WndProc 来处理消息,由 AllocateHwnd() 分配。最近我开始清理代码中的 WndProc,并决定删除辅助的 WndProc()。我更改了组件以覆盖基类 WndProc() 方法,并从那里进行自定义 Windows 消息处理。在那个 WndProc() 中,我首先调用了继承的处理程序,然后处理了我的自定义消息(WM_USER 偏移量),如果找到我的一条自定义消息并处理它,则将消息结果字段设置为 1。

一个重要的注意事项。我在 WndProc() 覆盖的顶部放置了一行代码,如果当前线程 id 不是 VCL 主线程,则会引发异常。我想确保 WndProc() 只在 VCL 主线程的上下文中执行。

这样做并运行我的程序后,我遇到了一些看起来非常奇怪的事情。我正常运行我的程序,并且没有错误地完成了各种任务。然后,当我转到与我的 TWinControl 后代位于同一页面上的 TMemo 控件时。如果我在该 TMemo 控件内单击,则会触发我的 WndProc() 覆盖中的主线程检查。我在上面设置了一个断点,当我进入调用堆栈时,在我的 WndProc() 覆盖之上没有任何内容。

据我所知,我已经仔细检查过,我没有显式调用 WndProc() 覆盖。那不是我曾经做过的事情。但是鉴于我的 TWinControl 组件将像所有其他组件一样在主 VCL 线程上创建,我无法理解 WndProc() 覆盖将如何在后台线程的上下文中执行,尤其是只有当 UI 操作像鼠标点击会发生。我了解我的 WndProc() 是如何与 TMemo 控件相关联的,因为所有子窗口 hang 都在顶级窗口 WndProc() 之外,至少这是我的理解。但是由于所有组件窗口都将在主 VCL 线程上创建,所以它们的所有消息队列也应该在该上下文中执行,对吧?

那么我可以创建什么样的情况来让我的 WndProc() 运行,而且只是有时,在后台线程的上下文中运行?

【问题讨论】:

    标签: delphi subclass message-queue vcl wndproc


    【解决方案1】:

    有两种方法可以在工作线程的上下文中调用主线程组件的WndProc() 方法:

    1. 工作线程直接调用组件的WindowProc属性或其Perform()方法。

    2. 工作线程通过不安全地使用TWinControl.Handle 属性窃取了组件窗口的所有权。 Handle 属性 getter 不是线程安全的。如果工作线程在主线程重新创建组件窗口的同时从Handle 属性读取(TWinControl 窗口不是持久的 - 各种运行时条件可以动态重新创建它们而不会影响您的大部分 UI 逻辑) ,则存在一个竞争条件,它可能允许工作线程在其自己的上下文中分配一个新窗口(并导致主线程泄漏另一个窗口)。这将导致主线程停止在其上下文中接收和发送消息。如果工作线程有自己的消息循环,那么它将改为接收和分派消息,从而在错误的线程上下文中调用WndProc() 方法。

    我觉得奇怪的是没有生成调用堆栈。应该总是有某种可用的跟踪。

    另外,请确保MainThreadId 变量(或用于跟踪主线程的任何变量)不会被意外损坏。确保其当前值与启动时的初始值一致。

    您应该做的另一件事是在调试器中命名所有线程实例(此功能在 Delphi 6 中引入)。这样,当您的线程验证被触发时,调试器可以向您显示调用 WndProc() 方法的线程上下文的确切名称(即使没有调用堆栈跟踪),然后您可以在代码中查找错误线程。

    【讨论】:

    • 尝试覆盖组件的CreateWnd()CreateWindowHandle() 方法并在调用线程上下文错误时引发您自己的异常。但实际上,永远不要从工作线程访问 TWinControl.Handle 属性,除非它被 TThread.Synchronize() 调用包装。
    • 与主线程同步,例如通过TThread.Synchronize()TThread.Queue(),是从工作线程访问TWinControl.Handle 属性的唯一安全方法。否则,让您的线程将它们的消息发送到TApplication.Handle 窗口,或者发送到由AllocateHWnd() 在主线程中创建的您自己的隐藏窗口。 HWNDs 中的任何一个都可以安全地在工作线程中访问,而无需与主线程同步。您的消息处理程序将在主线程中运行,并且可以根据需要安全地访问 UI 控件。
    • 与其让工作线程直接访问TMemo 或发送窗口消息,更安全的选择是让线程将其日志消息放入线程安全队列,例如@987654342 @ 或 Indy 的 TIdThreadSafeStringList,然后让主线程运行一个计时器,该计时器定期检查该队列是否有新的日志消息,并在它们出现时将它们添加到 TMemo。这样,工作线程就不再受到主线程速度的限制,主线程可以在准备好更新 UI 时自行更新。
    • TThread.Queue() 不会阻塞调用线程,它将请求排队,立即返回,并在后台处理请求。 TThread.Synchronize() 阻塞调用线程,等待请求处理完毕后再退出。
    • 使用PostMessage() 发布到TApplication.Handle 窗口的消息可以在TApplication.OnMessageTApplicationEvents.OnMessage 事件中处理。使用SendMessage...() 函数发送到窗口的消息可以通过使用TApplication.HookMainWindow() 方法注册消息处理程序来处理。
    【解决方案2】:

    Remy LeBeau 的回复 包含对我做错的解释。我包含此更新,因此您可以看到一个具体案例的棘手细节,该案例显示了在后台线程中保留对 VCL UI 控件的引用可能会产生多么微妙的错误。希望这些信息可以帮助您调试自己的代码。

    我的应用程序的一部分包括我创建的 VCL 组件,它源自 TCustomControl,而 TCustomControl 又源自 TWinControl。它聚合一个套接字,该套接字创建一个后台线程,用于从外部设备接收视频。

    当发生错误时,该后台线程使用 PostMessage() 将消息发布到 TMemo 控件以进行审计。 这就是我犯错误的地方,因为我与 PostMessage() 一起使用的窗口句柄 (HWND) 属于 TMemo 控件。 TMemo 控件与我的组件位于同一窗体上。

    当视频连接丢失时,为其提供服务的套接字将关闭并销毁,但事实证明为其提供服务的后台线程尚未退出。现在,当套接字尝试在其引用的失效套接字上执行操作时,会导致 #10038 套接字错误(对非套接字的操作)。这就是麻烦的开始。

    当它使用 TMemo 的句柄调用 PostMessage() 时,TMemo 处于必须按需重新创建句柄的状态,这是 Remy 描述的危险问题现象。这意味着重新创建的 TMemo 窗口中的 WndProc()现在正在后台线程的上下文中执行

    这符合所有证据。如上所述,我不仅在重写的 WndProc() 中收到后台线程警告,而且在 TMemo 窗口中使用鼠标完成的任何操作都会导致 #10038 错误消息流出现在 TMemo 中。发生这种情况是因为 TMemo、组件的重写 WndProc() 和后台线程之间现在存在松散耦合的循环条件,因为该线程在其 Execute() 方法中有一个 GetMessage 循环。

    每次将窗口消息发布到 TMemo 控件时,例如来自鼠标移动等,它都会在后台线程的消息队列中结束,因为它当前拥有 TMemo 后面的窗口。由于后台线程正在尝试退出并尝试在退出时关闭套接字,因此每次关闭尝试都会生成另一条 #10038 消息以发布到 TMemo,从而保持循环,因为现在每个 PostMessage() 本质上都是自发布.

    我已经为管理套接字在其析构函数中调用的后台线程的对象添加了一个通知方法,让线程知道它正在消失并且引用无效。我以前从未想过这样做,因为套接字在销毁期间关闭了后台线程,但是我不等待来自后台线程的终止事件。当然,另一种解决方案是等待后台线程终止。请注意,如果我采用了这种方法,那么这种情况最终会陷入死锁,而不是导致 TMemo 控件出现奇怪的行为。

    [对 Stack Overflow 编辑器的注意 - 我将此详细信息添加为回复,而不是修改原始消息,因此我不会将包含解决方案的 Remy 的答案推到页面下方。]

    【讨论】:

    • 这真的应该在原始消息中作为编辑发布,而不是作为它自己的答案。不要担心其他人的答案的位置。
    • @RemyLebeau。好的,我以后会这样做的。
    猜你喜欢
    • 1970-01-01
    • 2011-03-20
    • 1970-01-01
    • 2021-12-21
    • 2015-03-25
    • 1970-01-01
    • 1970-01-01
    • 2022-01-10
    • 1970-01-01
    相关资源
    最近更新 更多