4

我有一个多线程的 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 控件相关联,因为所有子窗口都在顶层窗口 WndProc() 之外,至少这是我的理解。但是由于所有组件窗口都将在主 VCL 线程上创建,因此它们的所有消息队列也应该在该上下文中执行,对吗?

那么我可以创建什么样的情况来使我的 WndProc() 运行,并且仅在某些情况下,在后台线程的上下文中运行?

4

2 回答 2

5

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

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

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

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

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

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

于 2012-01-30T06:46:19.550 回答
1

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 控件出现奇怪的行为。

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

于 2012-01-30T18:16:56.487 回答