пятница, 5 марта 2010 г.

В чём разница между PostMessage и SendNotifyMessage?

Это перевод C++ Q&A: Sending Messages in Windows. Автор: Paul DiLascia.

Есть несколько тонких различий в способах, которыми вы можете отправлять сообщения в Windows®, но основным различием между PostMessage и SendNotifyMessage является то, что функции семейства SendMessage отправляют (send) сообщение в другое окно немедленно, с помощью прямого вызова их оконной процедуры и ожидания ответа, в то время как PostMessage заносит сообщение в очередь в виде записи TMessage и возвращает управление немедленно, без ожидания.

С SendMessage, принимающая сторона обрабатывает сообщение немедленно, вместо того, чтобы обработать его в очереди в порядке поступления. Например, предположим, что вы написали:
var
Wnd: HWND;
S: String;
...
SendMessage(Wnd, WM_SETTEXT, 0, Integer(PChar('Fooblitzky')));
SetLength(S, GetWindowTextLength(Wnd));
GetWindowText(Wnd, PChar(S), Length(S));
SetLength(S, StrLen(PChar(S)));
// S = 'Fooblitzky'
Этот пример отправляет сообщение WM_SETTEXT в любое окно, которое записано в Wnd. Windows вызывает оконную процедуру этого окна с WM_SETTEXT в качестве идентификатора сообщения, нулём и 'Fooblitzky' как wParam и lParam. Оконная процедура обрабатывает сообщение и возвращает управление; управление возвращается на следующую строчку, которая вызывает GetWindowText. Как вы и ожидаете, S примет значение 'Fooblitzky'. Т.е. SendMessage ведёт себя как вызов функции, где WM_SETTEXT - это функция для вызова.

Что произойдёт, если вместо SendMessage вы используете PostMessage?
var
Wnd: HWND;
S: String;
...
PostMessage(Wnd, WM_SETTEXT, 0, Integer(PChar('Fooblitzky')));
SetLength(S, GetWindowTextLength(Wnd));
GetWindowText(Wnd, PChar(S), Length(S));
SetLength(S, StrLen(PChar(S)));
// S = ???
PostMessage не вызывает оконную процедуру. Вместо этого, она добавляет запись TMessage в очередь сообщений потока или процесса, который владеет Wnd, и возвращает управление немедленно. TMessage хранит номер (идентификатор) сообщения и его параметры. Поток или приложение обрабатывают сообщение, когда они доходят до него. Т.е. когда цикл выборки сообщений вызывает GetMessage и она возвращает это WM_SETTEXT (у нас могут быть и другие сообщения в очереди до нашего WM_SETTEXT). В любом случае, PostMessage возвращает управление немедленно, не ожидая ничего этого, так что когда начинает выполняться следующая строка (GetWindowText), то S не будет равно 'Fooblitzky', если только текст окна не был таким до того, как вы вызвали PostMessage (ну, если Wnd принадлежит другому потоку, то теоретически есть возможность, что ваш поток будет приостановлен в аккурат между вызовами PostMessage и GetWindowText, что даст другому потоку возможность успеть обработать WM_SETTEXT. Но это примерно так же вероятно, как то, что все молекулы в вашем теле неожиданно одновременно сместятся влево из-за редчайшего совпадения, что, скажем честно, навряд ли случится. Ну, OK, может быть этот сценарий не настолько маловероятен).

Итак, есть два способа доставки сообщения окну: SendMessage вызывает оконную процедуру напрямую; PostMessage использует очередь. Рисунок ниже иллюстрирует разницу (AfxWndProc - это оконная процедура):
SendMessage versus PostMessageТеперь, держа всё это в голове, мы можем рассмотреть другой вопрос: когда вы должны использовать SendMessage, а когда - PostMessage? В большинстве случаев, SendMessage делает то, что вы хотите. Она вызывает оконную процедуру для выполнения действия немедленно. Вы хотите сменить текст, рисунок или какое-то свойство - и вы хотите сделать это сейчас, потому что следующая строчка кода зависит от результата изменения. Поэтому вы часто можете видеть функции-оболочки, которые не делают ничего, кроме отправки сообщения. Например:
procedure TCustomEdit.CutToClipboard;
begin
SendMessage(Handle, WM_CUT, 0, 0);
end;
Одно из преимуществ таких функций-оболочек: они делают SendMessage более похожим на вызов функции (другое преимущество заключается в меньшем количестве кода для набора).

Когда вам следует использовать PostMessage? Обычно вы посылаете (post) сообщение, когда запрашиваемое действие не является срочным и/или его обработка занимает много времени, либо же вы не хотите прерывать текущую обработку для ожидания. Эти сообщения имеют характеристики уведомлений: "эй, что-то сейчас только что произошло - на случай, если тебя это волнует". Например, когда вы вызываете InvalidateRect или InvalidateRgn для уведомления Windows, что конкретная область нуждается в перерисовке, то Windows не перерисовывает область немедленно; вместо этого, она добавляет область для обновления в список и посылает сообщение WM_PAINT (если оно ещё не было послано). Если ваше приложение снова вызывает InvalidateRect, второй регион просто добавляется в список. Окно не перерисовывается, пока WM_PAINT не станет обрабатываться. Таким образом, несколько регионов могут быть обновлены за одну операцию рисования.

Похожая ситуация возникает, когда рабочий поток уведомляет свой UI-поток, что готова новая порция данных для обработки, с помощью посылки сообщения. Рабочий поток продолжает обработку следующей порции данных, а UI-поток может обработать сообщение, когда он до него доберётся. Асинхронное обновление - это довольно частая парадигма в программировании и это очевидный кандидат для PostMessage. В общем, PostMessage - "безопаснее", когда вы хотите отправить сообщение в другой поток или процесс, потому что SendMessage может блокировать выполнение. Мы поговорим об этом чуть позже.

Другим подходящим случаем для использования PostMessage является случай, когда вы хотите закончить обработку текущего сообщения перед обработкой нового. Например, предположим, что вы обрабатываете сообщение и вы определили, что настало время выйти из приложения (наверное, обрабатываемое сообщение - это команда "Выход"). Вы не хотите отправлять сообщение WM_QUIT через SendMessage; это вызвало бы оконную процедуру и привело бы к немедленному выходу - прямо в середине того, что бы вы ни делали в данный момент. Лучше послать WM_QUIT через PostMessage, так что ваше приложение сможет обработать его (и все другие bye-bye-сообщения типа WM_CLOSE, WM_DESTROY и WM_POSTNCDESTROY) уже после того, как вы закончите все свои дела. Этот пример настолько часто встречается, что у нас есть даже специальная функция для него, называемая PostQuitMessage.

PostMessage обычно работает лучше, когда вы хотите эмулировать команду или событие ввода посылкой WM_COMMAND или сообщений клавиатуры и мыши. Это потому что "настоящие" события ввода обычно приходят в виде последовательности связанных сообщений (таких как пары keydown/keyup) и ваше приложение может быть озадачено, если вы попробуете обработать новое сообщение в середине такой последовательности. В общем, посылка работает лучше отправки при эмуляции ввода.

Иногда вы можете использовать PostMessage, чтобы обойти подводный камень или баг для избежания бесконечной рекурсии. Например, предположим, что ваш обработчик сообщения WM_SETFOCUS определяет, что новое сфокусированное окно не подходит по какой-то причине, поэтому он решает передать фокус другому окну. Если вы вызовете SetFocus прямо из вашего обработчика, то Windows немедленно отправит другое сообщение WM_SETFOCUS, пока вы всё ещё обрабатываете старое! В результате вы получите бесконечный цикл, пока не исчерпается ваш стек. Чтобы избежать этого, вы можете послать сообщение самому себе (некое WM_MYSWITCHFOCUS), так что текущий обработчик может завершить свою работу до того, как вы смените фокус. Это один из тех примеров, которые проще понять на практике, чем на бумаге. Главной вещью, которой надо запомнить: Windows не позволит вам вызвать SetFocus из обработчика WM_SETFOCUS (это примерно то же самое, как вызывать скрытие окна из его OnShow).

Прим.пер.: другим примером является "удаление компонента из самого себя", часто для этого используется метод Release (или аналог), который, впрочем, тоже имеет свои особенности.

Поскольку SendMessage вызывает оконную процедуру напрямую, то ей нужен HWND. Как иначе вы узнаете, чью оконную процедуру нужно вызвать? Но PostMessage добавляет сообщение в очередь сообщений, которая ассоциирована с потоком или процессом, а не с окном. Теоретически, PostMessage не нужно окно. И это действительно так на самом деле:
  // отправить сообщение самому себе
PostMessage(0, WM_HI_THERE_HANDSOME, ...);
Если HWND равно 0, то PostMessage посылает сообщение в очередь текущего потока. На практике, однако, эта возможность не является ужасно полезной, поскольку в большинстве случаев вы хотите послать сообщение другому потоку (вероятно с использованием PostThreadMessage); но всегда могут быть редкие ситуации, когда просто удобно послать сообщение своему потоку без привязки к окну (если у вас есть практический пример - дайте мне знать).

В случае, если вы думаете, что начали понимать, когда следует использовать SendMessage, а когда - PostMessage, то вам следует знать, что у нас есть и другие функции отправки сообщения, о которых вам следует знать: SendMessageCallback, SendNotifyMessage и SendMessageTimeout. Эти функции весьма полезны в пьянящем мире Win32® и многопоточности. В Win32, если вы вызываете SendMessage, ваш поток оказывается заблокирован, пока получатель не обработает сообщение. Но если поток-получатель сам заблокирован, то SendMessage не возвращает управления. Ой.

SendNotifyMessage, SendMessageTimeout и SendMessageCallback были изобретены, чтобы обойти эту проблему. PostMessage, SendMessageTimeout и SendNotifyMessage являются хорошими кандидатами для вызова, если вы хотите сделать широковещательную рассылку всем окнам верхнего уровня, используя специальную константу HWND_TOPMOST в качестве окна HWND. Использовать же это значение вместе с SendMessage будет не самой отличной идеей, так как подвисший процесс заблокирует и вас тоже.

Чтобы просуммировать отличия PostMessage от SendNotifyMessage нужно особенно уточнить тот факт, что есть два типа сообщений: которые идут в очередь (queued) и те, которые не идут в очередь (non-queued). Post - это в очередь, Send - это в обход очереди. Это основное различие двух семейств функций:
  • Когда вы посылаете (post) сообщения в свой или другой поток, используя PostMessage, они идут в "очередь сообщений".
  • Когда вы отправляете (send) сообщения в свой поток, используя SendMessage, то сообщение обрабатывается немедленно, путём прямого вызова оконной процедуры прямо из SendMessage.
  • Когда вы отправляете (send) сообщения в другой поток, используя SendMessage, они технически не идут в "очередь сообщений", а скорее в "особое место", отличное от очереди сообщений потока. Но само сообщение будет обработано (т.е. вызвана оконная процедура) только когда поток вызывает одну из функций Win32 API, которая явно или неявно проверяет это "особое место" и автоматически вызывает оконную процедуру для каждого сообщения в нём. В этом случае SendMessage не вызывает оконную процедуру, но ждёт завершения её вызова.
  • Когда вызывается PeekMessage/GetMessage, она сначала проверяет, есть ли какие-то сообщения в "особом месте" и обрабатывает их автоматически. После чего она проверяет "очередь сообщений" и возвращает первый подходящий результат.

Функция GetQueueStatus сообщает вам, есть ли в очереди сообщений сообщения определённого типа. Поскольку у неё есть флаг QS_SENDMESSAGE, можно было бы подумать, что отправленные сообщения на самом деле тоже встают очередь сообщений. Однако это не так. Функции, которые работают с очередью сообщений, не обрабатывают сообщения из "особого места". Например, сообщения от SendMessage не фильтруются. Вместо этого, сообщения из "особого места" обрабатываются в некоторых функциях WinAPI, когда поток не занят. В некотором смысле, отправленные сообщения имеют более высокий приоритет, чем посланные в очередь, потому что они проверяются перед проверкой "очереди сообщений". И просто потому, что GetQueueStatus может сообщить вам статус "особого места", ещё не означает, что эти сообщения сидят в "очереди сообщений". QS_SENDMESSAGE - это просто обходное решение, чтобы не добавлять новую функцию.

Таблица ниже суммирует различия между разными функциями отправки сообщений (уфф!):
Функция
Назначение
Когда использовать
Версии Windows
SendMessage
Отправляет сообщение другому окну немедленно, вызывая его оконную процедуру. Возвращает управление, когда сообщение будет обработано. Думайте о ней, как о вызове другой функции, указанной сообщением.
Используйте для отправки сообщений в свои окна для выполнения действия немедленно, потому что следующие строки кода рассчитывают на завершения действия. SendMessage обычно используется для взаимодействия между дочерними/родительскими окнами. Будьте осторожны при отправке сообщений другим окнам, т.к. они могут висеть.
Все версии
PostMessage
Посылает сообщение другому окну постановкой сообщения в очередь потока, ассоциированного с окном. Возвращает управление немедленно, без ожидания завершения обработки сообщения. Может использоваться с нулевым HWND для отправки сообщения без окна.
Используйте для сообщений, которые не критичны к обработке, такими как уведомления. Эта функция также может быть использована для эмулирования команд или ввода; для отправки сообщений другим потокам без блокировки; и для широковещательной посылки сообщений (HWND_TOPMOST вместо окна) всем окнам верхнего уровня.
Все версии
SendNotifyMessage
Действует как SendMessage, только не ждёт обработки сообщения. Отличие от PostMessage в том, что сообщение не ставится в очередь. Кроме того, для своих окон гарантируется завершение обработки до выхода из функции.
Используйте для избежания блокировки от SendMessage, когда вы отправляете сообщения другим потокам, и при этом вас не интересует результат и/или время окончания обработки сообщений. Используйте с HWND_TOPMOST для широковещательной рассылки всем окнам верхнего уровня.
Win32
SendMessageTimeout
Аналогична SendMessage, но ждёт обработки лишь ограниченное количество времени.
Используйте для избежания блокировки при отправке сообщений другим потокам. Используйте с HWND_TOPMOST для широковещательной отправке всем окнам верхнего уровня.
Win32
SendMessageCallback
Отправляет сообщение окну и возвращает управление немедленно. Когда сообщение обрабатывается, Windows вызывает вашу функцию обратного вызова для сообщения ей результат.
Используйте эту функцию, когда вы хотели бы использовать PostMessage, но хотите знать о завершении обработки. Используйте её, когда вы хотите использовать SendMessage (не с широковещательной рассылкой), но не хотите блокировать поток.
Win32

6 комментариев:

  1. но всегда могут быть редкие ситуации, когда просто удобно послать сообщение своему потоку без привязки к окну (если у вас есть практический пример - дайте мне знать).

    Могу дать практический пример, но, т.к.
    он delphi specific, то врядли он будет
    интересен уважаемому Paul DiLascia.

    ОтветитьУдалить
  2. Практический пример - посылка сообщений из одного потока другому в консольном или безоконном приложении. Благополучно используется в течение многих лет.

    ОтветитьУдалить
  3. >>> Практический пример - посылка сообщений из одного потока другому в консольном или безоконном приложении
    Там шла речь об отправке сообщения самому себе через PostMessage(0, ...), а не другому потоку.

    А вообще, с отправкой сообщений потоку хорошо бы не забывать про это.

    ОтветитьУдалить
  4. >>>Там шла речь об отправке сообщения самому себе

    Извиняюсь, слона-то я и не приметил.

    >>>А вообще, с отправкой сообщений потоку хорошо бы не забывать про это.

    Безусловно, но в приложении, из которого я привел неудачный пример, используется MsgWaitForMultipleObjects и PeekMessage

    ОтветитьУдалить
  5. GunSmoker, всегда с удовольствием читаю твой блог.
    В частности эта статья очень помогла в решении проблемы общения порожденных потоков и основного vcl потока.

    ОтветитьУдалить
  6. "если у вас есть практический пример - дайте мне знать"
    Собственно, упомянутый автором же случай "самоуничтожения" объекта

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.