Как мог WndProc () потомка Delphi 6 TWinControl иногда выполняться вне основного потока VCL? - PullRequest
4 голосов
/ 30 января 2012

У меня есть приложение Delphi 6, которое является многопоточным. У меня есть компонент, который я создал, который происходит от TWinControl. Когда я впервые его построил, я использовал скрытое окно и его WndProc для обработки сообщений, выделенных с помощью AllocateHwnd (). Недавно я начал очищать WndProc в своем коде и решил удалить вспомогательный WndProc (). Я изменил компонент, чтобы вместо него переопределить метод WndProc () базового класса и оттуда обработал его пользовательские сообщения Windows. В этом WndProc () я сначала вызвал унаследованный обработчик, а затем обработал свои пользовательские сообщения (смещения WM_USER), установив для поля «Результат сообщения» значение 1, если нашел одно из моих пользовательских сообщений и обработал его.

Одно важное замечание. Я поместил строку кода в верхнюю часть переопределения WndProc (), которое создает исключение, если идентификатор текущего потока не является основным потоком VCL. Я хотел убедиться, что WndProc () выполняется только в контексте основного потока VCL.

После этого и запуска моей программы я столкнулся с чем-то действительно странным. Я запускал свою программу как обычно и выполнял различные задачи без ошибок. Затем, когда я перешел к элементу управления TMemo, который находится на той же странице, что и мой потомок TWinControl. Если я щелкнул внутри этого элемента управления TMemo, проверка главного потока в моем срабатывании WndProc () сработала. У меня была установлена ​​точка останова, и когда я перешел в стек вызовов, там не было ничего, кроме моего переопределения WndProc ().

Насколько я могу судить, и я дважды проверил, я не делаю явных вызовов для переопределения WndProc (). Это не то, что я когда-либо делал. Но, учитывая, что мой компонент TWinControl был бы создан в основном потоке VCL, как и все другие компоненты, я не могу понять, как переопределение WndProc () будет когда-либо выполняться в контексте фонового потока, особенно только когда действие пользовательского интерфейса, подобное щелчок мыши произойдет. Я понимаю, как мой WndProc () привязан к элементу управления TMemo, поскольку все дочерние окна висят вне окна верхнего уровня WndProc (), по крайней мере, это мое понимание. Но поскольку все окна компонентов были бы созданы в главном потоке VCL, то все их очереди сообщений должны выполняться и в этом контексте, верно?

Так какую же ситуацию я мог создать, чтобы мой WndProc () работал, и только иногда, в контексте фонового потока?

Ответы [ 2 ]

5 голосов
/ 30 января 2012

Существует два способа, которыми метод WndProc() компонента основного потока может быть вызван в контексте рабочего потока:

  1. рабочий поток напрямую вызывает свойство WindowProc компонентаили его метод Perform().

  2. рабочий поток похитил владение окном компонента из-за небезопасного использования свойства TWinControl.Handle.Получатель свойства Handle не является потокобезопасным.Если рабочий поток читает из свойства Handle в тот же момент, когда основной поток воссоздает окно компонента (окна TWinControl не являются постоянными - различные условия времени выполнения могут динамически воссоздавать их, не затрагивая большую часть вашей логики пользовательского интерфейса)затем существует условие состязания, которое может позволить рабочему потоку выделить новое окно в своем собственном контексте (и заставить основной поток пропустить другое окно).Это приведет к тому, что основной поток прекратит получать и отправлять сообщения в своем контексте.Если рабочий поток имеет свой собственный цикл обработки сообщений, он вместо этого будет получать и отправлять сообщения, вызывая метод WndProc() в неверном контексте потока.

Мне кажется странным, что нетСтек вызовов производится, однако.Всегда должна быть доступна какая-то трассировка.

Кроме того, убедитесь, что переменная MainThreadId (или то, что вы используете для отслеживания основного потока) не просто повреждается случайно.Убедитесь, что его текущее значение соответствует его начальному значению при запуске.

Еще одна вещь, которую вы должны сделать, это назвать все ваши экземпляры потока в отладчике (эта функция была введена в Delphi 6).Таким образом, когда проверка вашего потока сработает, отладчик может показать вам точное имя контекста потока, который вызывает ваш метод WndProc() (даже без трассировки стека вызовов), тогда вы можете искать ошибки в коде для этогонить.

1 голос
/ 30 января 2012

Ответ Реми Лебо содержит объяснение того, что я сделал не так. Я включил это обновление, чтобы вы могли увидеть хитрые детали конкретного случая, который показывает, насколько тонкой может быть ошибка, сохраняющая ссылку на элемент управления VCL UI в фоновом потоке. Надеюсь, эта информация поможет вам отладить ваш собственный код.

Часть моего приложения включает созданный мною компонент VCL, который происходит от TCustomControl, который, в свою очередь, происходит от TWinControl. Он объединяет сокет, и этот сокет создает фоновый поток для приема видео с внешнего устройства.

При возникновении ошибки этот фоновый поток отправляет сообщение в элемент управления TMemo для целей аудита с использованием PostMessage (). Именно здесь я допустил ошибку, потому что дескриптор окна (HWND), который я использую с PostMessage (), принадлежит элементу управления TMemo. Элемент управления TMemo находится в той же форме, что и мой компонент.

Когда теряется видеосоединение, сокет, обслуживающий его, закрывается и уничтожается, но оказывается, что фоновый поток, обслуживающий его, еще не завершился. Теперь, когда сокет пытается выполнить операцию на несуществующем сокете, на который он ссылается, это приводит к ошибке сокета # 10038 (операция не для сокета). Вот тут и начинается беда.

Когда он вызывает PostMessage () с дескриптором TMemo, TMemo находится в состоянии, в котором он должен воссоздать дескриптор по требованию , явление коварной проблемы, которое описывает Реми. Это означает, что WndProc () в воссозданном окне TMemo теперь выполняется в контексте фонового потока .

Это соответствует всем доказательствам. Мало того, что я получаю предупреждение фонового потока в моем переопределенном WndProc (), как упомянуто выше, но все, что делается в окне TMemo с помощью мыши, вызывает поток сообщений об ошибках # 10038 в TMemo. Это происходит потому, что теперь существует слабосвязанное циклическое условие между TMemo, переопределенным компонентом WndProc () и фоновым потоком, поскольку этот поток имеет цикл GetMessage в своем методе Execute ().

Каждый раз, когда сообщение окна отправляется в элемент управления TMemo, например, от движений мыши и т. Д., Оно попадает в очередь сообщений фонового потока, поскольку в настоящее время ему принадлежит окно за TMemo. Так как фоновый поток пытается выйти и пытается закрыть сокет при выходе, каждая попытка закрытия генерирует другое сообщение # 10038, которое будет отправлено в TMemo, сохраняя цикл, потому что каждый PostMessage () по сути теперь является самопубликацией. .

С тех пор я добавил метод уведомления к объекту, который управляет фоновым потоком, который сокет вызывает в своем деструкторе, позволяя потоку узнать о его исчезновении и что ссылка недействительна. Я никогда не думал делать это раньше, потому что сокет закрывает фоновый поток во время уничтожения, однако я не жду события завершения из фонового потока. Конечно, альтернативным решением было бы дождаться завершения фонового потока. Обратите внимание, что если бы я применил этот подход, этот сценарий оказался бы в тупике, а не привел бы к странному поведению с элементом управления TMemo.

[ПРИМЕЧАНИЕ для редактора Stack Overflow - я добавляю эту деталь в качестве ответа, а не изменяю исходное сообщение, чтобы не толкать ответ Реми, содержащий решение, далеко вниз по странице.]

...