Delphi 6 TWinControl 后代的 WndProc() 有时如何在主 VCL 线程之外执行?

发布于 2024-12-29 17:45:37 字数 939 浏览 6 评论 0原文

我有一个高度多线程的 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() 运行,并且仅有时在后台线程的上下文中运行?

I have a Delphi 6 application that is heavily multithreaded. I have a component I created that descends from TWinControl. When I first built it, I used a hidden window and it's WndProc to handle messages, allocated with AllocateHwnd(). Recently I started cleaning up the WndProc's in my code and decided to remove the auxiliary WndProc(). I changed the component to override the base class WndProc() method instead and do its custom windows message handling from there. In that WndProc() I called the inherited handler first and then processed my custom messages (WM_USER offsets), setting the message Result field to 1 if found one of my custom messages and handled it.

One important note. I put a line of code at the top of the WndProc() override that throws an Exception if the current thread id is not the VCL main thread. I wanted to make sure that the WndProc() only executed in the context of the main VCL thread.

After doing this and running my program I ran into something that seems truly bizarre. I ran my program as normal and did various tasks without error. Then, when I went to a TMemo control that resides on the same page as my TWinControl descendant. If I clicked inside that TMemo control the main thread check in my WndProc() override triggered. I had a breakpoint set on it and when I went to the call stack, there was nothing on it above my WndProc() override.

As far as I can tell, and I've double checked, I do not make explicit calls to the WndProc() override. That's not something I'd ever do. But given that my TWinControl component would have been created on the main VCL thread like all the other components, I can't fathom how the WndProc() override would ever execute in the context of a background thread, especially only when a UI action like a mouse click would happen. I understand how my WndProc() is tied to the TMemo control since all child windows hang off the top level window WndProc(), at least that's my understanding. But since all the component windows would have been created on the main VCL thread, then all their message queues should be executing in that context too, right?

So what kind of a situation could I have created to make my WndProc() run, and only sometimes, in the context of a background thread?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

勿忘初心 2025-01-05 17:45:37

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

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

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

不过,我发现没有生成任何调用堆栈,这很奇怪。应该总是有某种可用的跟踪。

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

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

There are two ways a main thread component's WndProc() method could be called in the context of a worker thread:

  1. the worker thread directly calls into the component's WindowProc property, or its Perform() method.

  2. the worker thread has stolen ownership of the component's window through unsafe usage of the TWinControl.Handle property. The Handle property getter is not thread safe. If a worker thread reads from the Handle property at the exact same moment that the main thread is recreating the component's window (TWinControl windows are not persistent - various runtime conditions can dynamically recreate them without affecting the majority of your UI logic), then there exists a race condition that could allow the worker thread to allocate a new window within its own context (and cause the main thread to leak another window). That would cause the main thread to stop receiving and dispatching messages within its context. If the worker thread has its own message loop then it would receive and dispatch the messages instead, thus calling the WndProc() method in the wrong thread context.

I find it odd that no call stack is being produced, though. There should always be some sort of trace available.

Also, make sure the MainThreadId variable (or whatever you are using to track the main thread) is not simply getting corrupted by accident. Make sure its current value is consistent with its initial value from startup.

Another thing you should do is name all of your thread instances in the debugger (this feature was introduced in Delphi 6). That way, when your thread validation gets tripped, the debugger can show you the exact name of the thread context that is calling your WndProc() method (even without a call stack trace), then you can look for bugs in the code for that thread.

っ左 2025-01-05 17:45:37

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

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

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

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

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

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

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

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

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

Remy LeBeau' reply contains the explanation of what I did wrong. I am including this update so you can see the tricky details of a concrete case that shows just how subtle an error keeping a reference to a VCL UI control in a background thread can create. Hopefully this information should help you debug your own code.

Part of my application includes a VCL component I created that descends from TCustomControl who in turn descends from TWinControl. It aggregates a socket and that socket creates a background thread for receiving video from an external device.

When an error occurs, that background thread posts a message to a TMemo control for auditing purposes using PostMessage(). That is where I made my mistake because the window handle (HWND) I use with PostMessage() belongs to a TMemo control. The TMemo control resides on the same form as my component.

When a video connection is lost, the socket that services it is closed and destroyed, but it turns out the background thread servicing it has not exited yet. Now when the socket tries to execute an operation on the defunct socket that it has a reference to, it results in a #10038 socket error (operation on a non-socket). This is where the trouble starts.

When it calls PostMessage() with the TMemo's handle, the TMemo is in a state that it has to recreate the handle on demand, the treacherous problem phenomenon that Remy describes. This means the WndProc() in the recreated TMemo window is now executing in the context of the background thread.

This fits all the evidence. Not only do I get the background thread warning in my overridden WndProc() as mentioned above, but anything done in the TMemo window with the mouse causes a stream of #10038 error messages to appear in the TMemo. This is happening because a loosely coupled cyclic condition now exists between the TMemo, the component's overridden WndProc(), and the background thread, since that thread has a GetMessage loop in its Execute() method.

Every time a windows message is posted to the TMemo control, like from mouse movements, etc., it ends up in the background thread's message queue since it currently owns the window behind the TMemo. Since the background thread is trying to exit and it tries to close the socket on the way out, each close attempt generates another #10038 message to be posted to the TMemo, persisting the loop because each PostMessage() is essentially a self-post now.

I have since added a notification method to the object that manages the background thread that the socket calls in its destructor, letting the thread know its going away and that the reference is invalid. I never thought to do that before because the socket shuts the background thread down during destruction, however I don't wait for a termination event from the background thread. An alternative solution of course would be to wait for the background thread to terminate. Note, had I adopted that approach then this scenario would have ended up in a deadlock instead of it resulting in strange behavior with a TMemo control.

[NOTE to Stack Overflow editor - I am adding this detail as a reply instead of modifying the original message so I don't push Remy's answer that contains the solution far down the page.]

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文