Как может WndProc () потомка TWinControl из Delphi 6 иногда выполняться вне основного потока VCL?

У меня есть приложение 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 () работал, и только иногда, в контексте фонового потока?


person Robert Oschler    schedule 30.01.2012    source источник


Ответы (2)


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

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

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

Однако мне кажется странным, что стек вызовов не создается. Всегда должен быть какой-то след.

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

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

person Remy Lebeau    schedule 30.01.2012
comment
Спасибо. Я видел воссозданную проблему дескриптора, когда однажды я пострадал из-за недействительной ссылки на окно. Это одна из причин, по которой я инстинктивно обращаюсь к AllocateHWND (), когда не должен (я помню ваши комментарии по этому поводу из другого потока). Есть ли способ в коде, которым я мог бы поймать рабочий поток, который захватывает дескриптор в простой вид? У меня много потоковой активности, и я хотел бы быстро найти такую ​​проблему, если смогу, без полного сканирования кода. Что-то, что могло вызвать исключение, когда поток пытался получить доступ к дескриптору? - person Robert Oschler; 30.01.2012
comment
попробуйте переопределить метод CreateWnd() или CreateWindowHandle() компонента и вызвать собственное исключение, если контекст вызывающего потока неверен. Но на самом деле НИКОГДА не обращайтесь к свойству TWinControl.Handle из рабочего потока с самого начала, если оно не обернуто вызовом TThread.Synchronize(). - person Remy Lebeau; 30.01.2012
comment
Спасибо. Это как раз то, что мне было нужно. - person Robert Oschler; 30.01.2012
comment
Пожалуйста, посмотрите мой ответ ниже, где я подробно показываю, насколько важно ваше предупреждение о том, что не храните ссылки на оконные элементы управления пользовательского интерфейса в фоновом потоке. - person Robert Oschler; 30.01.2012
comment
каков безопасный способ получить дескриптор окна для элемента управления пользовательского интерфейса в основном потоке из фонового потока? Если его нет, как мне успешно публиковать сообщения в желаемый элемент управления пользовательского интерфейса из фонового потока? - person Robert Oschler; 30.01.2012
comment
Синхронизация с основным потоком, например через TThread.Synchronize() или TThread.Queue(), является единственным безопасным способом доступа к свойству TWinControl.Handle из рабочего потока. В противном случае пусть ваши потоки отправляют свои сообщения в TApplication.Handle окно или в ваше собственное скрытое окно, созданное AllocateHWnd() в основном потоке. Любой из этих HWNDs будет безопасным для доступа в рабочих потоках без синхронизации с основным потоком. Ваш обработчик сообщений будет работать в основном потоке и может безопасно обращаться к элементам управления пользовательского интерфейса по мере необходимости. - person Remy Lebeau; 01.02.2012
comment
Вместо того, чтобы рабочие потоки напрямую обращались к TMemo или отправляли оконные сообщения, более безопасной альтернативой было бы, чтобы потоки помещали свои сообщения журнала в потокобезопасную очередь, такую ​​как TThreadList или Indy TIdThreadSafeStringList, а затем запускали основной поток таймер, который периодически проверяет эту очередь на наличие новых сообщений журнала и добавляет их в TMemo, если они есть. Таким образом, рабочие потоки больше не регулируются скоростью основного потока, и основной поток может обновить пользовательский интерфейс в свое время, когда он будет готов к этому. - person Remy Lebeau; 01.02.2012
comment
Это то, что мне нужно было знать, стабильная ручка. Я не хочу использовать Synchronize () или Queue (), потому что я стараюсь любой ценой избегать блоков потоков. Но я могу отправить сообщение в TApplication.Handle. Я прочитаю, как обрабатывать сообщения, отправленные на этот дескриптор. - person Robert Oschler; 01.02.2012
comment
TThread.Queue() не блокирует вызывающий поток, он ставит запрос в очередь, немедленно возвращается и обрабатывает запрос в фоновом режиме. TThread.Synchronize() блокирует вызывающий поток и ожидает обработки запроса перед завершением. - person Remy Lebeau; 01.02.2012
comment
Сообщения, отправленные в окно TApplication.Handle с использованием PostMessage(), могут быть обработаны в событии TApplication.OnMessage или TApplicationEvents.OnMessage. Сообщения, отправленные в окно с использованием функций SendMessage...(), могут быть обработаны путем регистрации обработчика сообщений с использованием метода TApplication.HookMainWindow(). - person Remy Lebeau; 01.02.2012
comment
Да, но есть ли в Delphi 6 Queue ()? Я вижу только Synchronize () в справке. - person Robert Oschler; 01.02.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 ().

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

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

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

person Community    schedule 30.01.2012
comment
Это действительно должно было быть опубликовано как правка в исходном сообщении, а не как отдельный ответ. Не беспокойтесь о размещении ответов других людей. - person Remy Lebeau; 01.02.2012
comment
@RemyLebeau. Хорошо, я сделаю это в будущем. - person Robert Oschler; 01.02.2012