Лучше использовать «Синхронизировать» TThread или использовать сообщения окна для IPC между основным и дочерним потоком? - PullRequest
21 голосов
/ 27 ноября 2009

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

Мой первоначальный проект включал вызов TThread "Synchronize" для обновления формы управления сеткой в ​​текущем выполняющемся потоке. Однако я понимаю, что вызов Synchronize по существу выполняется так, как если бы он был основным потоком при вызове. При одновременном запуске до 16 потоков (и большая часть обработки дочернего потока занимает от <1 секунды до ~ 10 секунд), будет ли Window Messages лучшим дизайном? </p>

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

Есть ли мнения о лучшем методе для МПК в этой ситуации? Окно сообщений или «Синхронизировать»?

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

Ответы [ 3 ]

36 голосов
/ 27 ноября 2009

Изменить:

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

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

Спасибо за исправление. Так что возьмите детали в исходном ответе с крошкой соли.

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


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

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

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

Так что с ним не так (и что не так с использованием SendMessage() напрямую)? Несколько вещей:

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

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

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

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

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

Edit:

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

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

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

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

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

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

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

9 голосов
/ 27 ноября 2009

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

(Queue доступно с D8).

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

5 голосов
/ 27 ноября 2009

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

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

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

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

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

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

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

...