Что лучше - использовать "Синхронизацию” TThread или использовать оконные сообщения для IPC между основным и дочерним потоком?

StackOverflow https://stackoverflow.com/questions/1806339

  •  05-07-2019
  •  | 
  •  

Вопрос

У меня есть довольно простое многопоточное приложение с графическим интерфейсом VCL, написанное с помощью Delphi 2007.Я выполняю некоторую обработку в нескольких дочерних потоках (до 16 одновременных), которым необходимо обновить элемент управления grid в моей основной форме (просто отправляя строки в grid).Ни один из дочерних потоков никогда не разговаривает друг с другом.

Мой первоначальный замысел предполагал вызов "Синхронизировать" tthread's чтобы обновить форму управления сеткой в текущем запущенном потоке.Однако я понимаю, что вызов Synchronize по существу выполняется так, как если бы это был основной поток при вызове.При одновременном запуске до 16 потоков (и большая часть обработки дочернего потока выполняется из < от 1 секунды до ~ 10 секунд) будет ли дизайн оконных сообщений лучше?

Я заработал на этом этапе, когда дочерний поток публикует сообщение Windows (состоящее из записи из нескольких строк), а основной поток имеет прослушиватель и просто обновляет сетку при получении сообщения.

Есть какие-нибудь мнения о наилучшем методе IPC в данной ситуации?Оконные сообщения или "Синхронизировать"?

Если я использую оконные сообщения, предлагаете ли вы обернуть код, в котором я публикую, в сетку в блоке TCriticalSection (ввод и оставление)?Или мне не нужно будет беспокоиться о потокобезопасности, поскольку я пишу в сетку в основном потоке (хотя и в рамках функции обработчика сообщений window)?

Это было полезно?

Решение

Редактировать:

Похоже, что многие детали реализации изменились со времен Delphi 4 и 5 (версии Delphi, которые я все еще использую для большей части своей работы), и Аллен Бауэр прокомментировал следующее:

Начиная с D6, TThread больше не использует SendMessage.Он использует потокобезопасную рабочую очередь, в которую помещается "работа", предназначенная для основного потока.В основной поток отправляется сообщение, указывающее, что работа доступна, а фоновый поток блокирует событие.Когда основной цикл обмена сообщениями вот-вот перейдет в режим ожидания, он вызывает "CheckSynchronize", чтобы узнать, ожидает ли какая-либо работа.Если это так, то он обрабатывает это.Как только рабочий элемент завершен, событие, при котором блокируется фоновый поток, устанавливается для указания завершения.Представленный на таймфрейме D2006, был добавлен метод TThread.Queue, который не блокирует.

Спасибо за исправление.Так что отнеситесь к деталям, содержащимся в первоначальном ответе, со всей серьезностью.

Но на самом деле это никак не влияет на основные моменты.Я по-прежнему утверждаю, что вся идея Synchronize() является фатально испорченным, и это станет очевидно в тот момент, когда кто-то попытается занять несколько ядер современной машины.Не "синхронизируйте" свои потоки, позвольте им работать до тех пор, пока они не будут закончены.Постарайтесь свести к минимуму все зависимости между ними.Особенно при обновлении графического интерфейса нет абсолютно НЕТ причина дождаться завершения этого процесса.Будь то Synchronize() использование SendMessage() или PostMessage(), результирующий дорожный блок будет таким же.


То, что вы здесь представляете, вообще не является альтернативой, поскольку Synchronize() использование SendMessage() внутренне.Так что это скорее вопрос, из какого оружия вы хотите выстрелить себе в ногу.

Synchronize() работает с нами с момента появления TThread в VCL Delphi 2, что на самом деле является позором, поскольку это одна из самых больших ошибок дизайна в VCL.

Как это работает?Он использует SendMessage() вызов окна, которое было создано в основном потоке, и устанавливает параметры сообщения для передачи адреса вызываемого объектного метода без параметров.Поскольку сообщения Windows будут обрабатываться только в потоке, который создал окно назначения и запускает его цикл сообщений, это приведет к приостановке потока, обработке сообщения в контексте основного потока VCL, вызову метод и возобновлению потока только после завершения выполнения метода.

Так что же в этом плохого (и что такого же плохого в использовании SendMessage() непосредственно)?Несколько вещей:

  • Принуждение любого потока выполнять код в контексте другого потока приводит к переключению контекста двух потоков, что приводит к ненужному сжиганию циклов процессора.
  • Пока поток VCL обрабатывает сообщение для вызова синхронизированного метода, он не может обработать никакое другое сообщение.
  • Когда несколько потоков используют этот метод, они будут ВСЕ заблокируйте и дождитесь Synchronize() или SendMessage() чтобы вернуться.Это создает гигантское узкое место.
  • Существует тупик, который только и ждет своего часа.Если поток вызывает Synchronize() или SendMessage() при удержании объекта синхронизации и поток VCL при обработке сообщения должны получить тот же объект синхронизации, который приложение заблокирует.
  • То же самое можно сказать и о вызовах API, ожидающих дескриптора потока - используя WaitForSingleObject() или WaitForMultipleObjects() отсутствие каких-либо средств для обработки сообщений приведет к взаимоблокировке, если потоку понадобятся эти способы для "синхронизации" с другим потоком.

Так что же использовать вместо этого?Несколько вариантов, я опишу некоторые:

  • Использование PostMessage() вместо того , чтобы SendMessage() (или PostThreadMessage() если оба потока не являются потоком VCL).Однако важно не использовать какие-либо данные в параметрах сообщения, которые больше не будут действительными при поступлении сообщения, поскольку отправляющий и принимающий потоки вообще не синхронизированы, поэтому необходимо использовать некоторые другие средства, чтобы убедиться, что любая строка, ссылка на объект или фрагмент памяти все еще действительны при обработке сообщения, даже если отправляющий поток может даже больше не существовать.

  • Создавайте потокобезопасные структуры данных, помещайте в них данные из ваших рабочих потоков и используйте их из основного потока.Использование PostMessage() только для того, чтобы предупредить поток VCL о поступлении новых данных для обработки, но не отправляйте сообщения каждый раз.Если у вас есть непрерывный поток данных, вы могли бы даже провести опрос потока VCL для получения данных (возможно, с помощью таймера), но это только версия для бедных.

  • Больше вообще не используйте низкоуровневые инструменты.Если вы, по крайней мере, используете Delphi 2007, загрузите Многопоточная библиотека и начните мыслить в терминах задач, а не потоков.Эта библиотека обладает множеством возможностей для обмена данными между потоками и синхронизации.Он также имеет реализацию пула потоков, что хорошо - количество потоков, которые вы должны использовать, зависит не только от приложения, но и от оборудования, на котором оно запущено, поэтому многие решения могут быть приняты только во время выполнения.OTL позволит вам запускать задачи в потоке пула потоков, поэтому система может настраивать количество одновременных потоков во время выполнения.

Редактировать:

При перечитывании я понимаю, что вы не собираетесь использовать SendMessage() но PostMessage() - ну, тогда кое-что из вышесказанного неприменимо, но я оставлю это на месте.Однако в вашем вопросе есть еще несколько моментов, на которые я хочу обратить внимание:

При одновременном запуске до 16 потоков (и большая часть обработки дочернего потока выполняется из < от 1 секунды до ~ 10 секунд) будет ли дизайн оконных сообщений лучше?

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

где дочерний поток отправляет сообщение Windows (состоящее из записи из нескольких строк)

Оконное сообщение не может содержать запись.Он содержит два параметра, один из которых типа WPARAM, другой из типов LPARAM.Вы можете привести указатель на такую запись только к одному из этих типов, поэтому временем жизни записи нужно как-то управлять.Если вы выделяете его динамически, вам также необходимо освободить его, что чревато ошибками.Если вы передаете указатель на запись в стеке или на поле объекта, вам нужно убедиться, что он все еще действителен при обработке сообщения, что сложнее для опубликованных сообщений, чем для отправленных.

вы предлагаете обернуть код, в котором я публикую, в сетку в блоке TCriticalSection (ввод и оставление)?Или мне не нужно будет беспокоиться о потокобезопасности, поскольку я пишу в сетку в основном потоке (хотя и в рамках функции обработчика сообщений window)?

Нет необходимости делать это, так как PostMessage() вызов будет возвращен немедленно, поэтому синхронизация не требуется на данный момент.Вам определенно нужно будет побеспокоиться о потокобезопасности, к сожалению, вы не можете знать наверняка когда.Вы должны убедиться, что доступ к данным потокобезопасен, путем всегда блокировка данных для доступа с использованием объектов синхронизации.На самом деле нет способа добиться этого для записей, к данным всегда можно получить прямой доступ.

Другие советы

Кстати, вы также можете использовать TThread.Queue() вместо того , чтобы TThread.Synchronize(). Queue() это асинхронная версия, она не блокирует вызывающий поток:

(Queue доступен начиная с D8).

Я предпочитаю Synchronize() или Queue(), потому что это намного проще для понимания (для других программистов) и лучше OO, чем обычная отправка сообщений (без контроля над этим или возможности его отладки!)

Хотя я уверен, что есть правильный путь и неправильный.Я написал код, используя оба метода, и тот, к которому я продолжаю возвращаться, - это метод SendMessage, и я не уверен, почему.

Использование SendMessage против Synchronize на самом деле не имеет никакого значения.Оба работают, по сути, одинаково.Я думаю, причина, по которой я продолжаю использовать SendMessage, заключается в том, что я чувствую больший контроль, но я не знаю.

Процедура SendMessage заставляет вызывающий поток приостанавливаться и ждать, пока окно назначения не завершит обработку отправленного сообщения.Из-за этого основной поток приложения по существу синхронизируется с вызывающим дочерним потоком на время выполнения вызова.Вам не нужно использовать критический раздел в обработчике сообщений Windows.

Передача данных, по сути, осуществляется одним способом - из вызывающего потока в основной поток приложения.Вы можете вернуть значение целочисленного типа в message.result, но ничего, что указывало бы на объект памяти в основном потоке.

Поскольку в этот момент два потока "синхронизированы", а основной поток приложений в данный момент связан с ответом на SendMessage, вам также не нужно беспокоиться о том, что другие потоки приходят и одновременно уничтожают ваши данные.Так что нет, вам не нужно беспокоиться об использовании критических разделов или других типов мер потокобезопасности.

Для простых целей вы можете определить одно сообщение (wm_threadmsg1) и использовать поля wparam и lparam для передачи (целых) сообщений о состоянии туда и обратно.Для более сложных примеров вы можете передать строку, передав ее через lparam и преобразовав ее обратно в longint .A-la longint (pchar (myvar)) или используйте pwidechar, если вы используете D2009 или новее.

Если у вас уже есть программа, работающая с методами синхронизации, то я бы не стал беспокоиться о ее переработке для внесения изменений.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top