How could a Delphi 6 TWinControl descendant of WndProc () sometimes execute the main VCL thread?

I have a Delphi 6 application that is highly multithreaded. I have a component that I created that comes from TWinControl. When I first built it, I used a hidden window, and WndProc was processing the messages allocated by AllocateHwnd (). I recently started flushing WndProc in my code and decided to remove the helper WndProc (). I changed the component to instead override the WndProc () base class method and do the processing of custom Windows messages. In this WndProc (), I first called the inherited handler, and then processed my custom messages (WM_USER offsets), setting the Message field to โ€œ1โ€ if I found one of my custom messages and processed it.

One important point. 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 main VCL thread. I wanted to make sure that WndProc () runs only in the context of the main VCL thread.

After that and running my program, I came across something that seems really bizarre. I ran my program as usual and performed various tasks without errors. Then, when I went to the TMemo control, which is on the same page as my descendant TWinControl. If I clicked inside, that TMemo controls the main thread check in my WndProc () override. I had a breakpoint on it, and when I went to the call stack, there was nothing above my WndProc () override.

As far as I can tell, and I've double-checked, I don't make explicit WndProc () override calls. This is not what I have ever done. But considering that my TWinControl component would be created in the main VCL thread, like all other components, I canโ€™t understand how WndProc () redefinition will be performed in the context of the background thread, especially when a UI action, for example, when a mouse click occurs. I understand how my WndProc () is bound to the TMemo control, since all child windows hang at the top level of the WndProc () window, at least in my understanding. But since all component windows would be created in the main VCL thread, then all message queues should also be executed in this context, right?

So, what situation could I create to execute my WndProc (), and only sometimes, in the context of a background thread?

+4
source share
2 answers

There are two ways to call the main thread method WndProc() in the context of a workflow:

  • the worker thread directly calls the WindowProc property or its Perform() method.

  • the workflow has stolen ownership of the component window due to the unsafe use of the TWinControl.Handle property. The Handle property attribute is not thread safe. If a workflow reads from the Handle property at the same time that the main thread recreates the component window ( TWinControl windows are not constant - various execution conditions can dynamically recreate them without affecting most of your user interface logic), then there is a race condition that may allow a worker thread to highlight a new window in its own context (and cause the main thread to leak another window). This will cause the main thread to stop receiving and sending messages in its context. If the worker thread has its own message loop, it will receive and send messages instead, thereby calling the WndProc() method in the wrong thread context.

It seems strange to me that not a single call stack is created. There should always be some kind of trace available.

Also, make sure that the MainThreadId variable (or whatever you use to track the main thread) is not just accidentally corrupted. Verify that its current value matches its initial value at startup.

Another thing you have to do is to name all instances of the stream in the debugger (this function was introduced in Delphi 6). Thus, when your thread validation is turned off, the debugger can show you the exact name of the context of the thread calling your WndProc() method (even without tracing the call stack), then you can look for errors in the code for this thread.

+5
source

Remy LeBeau's answer provides an explanation of what I did wrong. I am including this update so that you can see the tricky details of a particular case, which shows how subtle a bug might be that contains a link to a VCL UI control in the background thread. Hope this information helps you debug your own code.

Part of my application includes the VCL component that I created, which comes from TCustomControl, which in turn descends from TWinControl. It combines a socket, and this socket creates a background stream for receiving video from an external device.

If an error occurs, this background thread sends a message to the TMemo controller for audit purposes using PostMessage (). This is where I made a mistake because the window handle (HWND) that I use with PostMessage () belongs to the TMemo control. The TMemo element is in the same form as my component.

When the video connection is lost, the socket that serves it is closed and destroyed, but it turns out that the background thread serving it has not yet come out. Now, when a socket tries to perform an operation on an unused socket that it refers to, this results in socket error # 10038 (operation on a non-socket). This is where the problem begins.

When he calls PostMessage () with a TMemo descriptor, TMemo is in a state in which it should recreate the descriptor on demand , an insidious phenomenon phenomenon that Remy describes. This means that WndProc () in the recreated TMemo window now runs in the context of the background thread.

This is consistent with all evidence. Not only do I get a warning about the history in my overridden WndProc (), as mentioned above, but everything that was done in the TMemo window with the mouse causes a stream of error messages # 10038 in TMemo. This is because there is a loosely coupled loop condition between TMemo, the component overridden by WndProc () and the background thread, because this thread has a GetMessage loop in the Execute () method.

Each time a Windows message is sent to a TMemo control, for example, with mouse movements, etc., it ends in the message flow of the background thread, since it currently belongs to the window behind TMemo. As the background thread tries to exit and it tries to close the socket on the output, each attempt to close creates another message # 10038, which should be sent to TMemo, saving a loop, because each PostMessage () is essentially a self-starting.

Since then, I have added a notification method to an object that controls the background thread that the socket calls in its destructor, letting the thread know that it is leaving and that the link is incorrect. I had never thought about this before, because the socket disables the background thread during destruction, however I am not waiting for the completion event from the background thread. An alternative solution, of course, would be to wait for the background thread to complete. Notice that if I took this approach, then this script would be stalled instead of it, which would lead to strange behavior with the TMemo control.

[NOTE to the editor. I am adding this detail as an answer instead of modifying the original post, so I do not click on the Remy answer that contains the solution, far down the page.]

+1
source

Source: https://habr.com/ru/post/1393695/


All Articles