Ответ 1
Edit:
Похоже, что многие детали реализации были изменены с Delphi 4 и 5 (версии Delphi, которые я до сих пор использую для большей части моей работы), и Аллен Бауэр прокомментировал следующее:
С тех пор, как D6, TThread больше не использует SendMessage. Он использует поточную рабочую очередь, где размещается "работа", предназначенная для основного потока. Сообщение отправляется в основной поток, чтобы указать, что работа доступна, и блоки потока фона в событии. Когда основной цикл сообщения вот-вот начнет работать, он вызывает "CheckSynchronize", чтобы увидеть, ожидает ли какая-либо работа. Если это так, он обрабатывает его. Как только рабочий элемент будет завершен, событие, на котором заблокирован фоновый поток, установлено, чтобы указать завершение. Добавленный в D2006 таймфрейм, был добавлен метод TThread.Queue, который не блокируется.
Спасибо за исправление. Поэтому возьмите детали в исходном ответе с солью.
Но это не влияет на основные моменты. Я все еще утверждаю, что вся идея Synchronize()
является смертельно ошибочной, и это будет очевидно в тот момент, когда вы пытаетесь удержать несколько ядер современной машины. Не синхронизируйте свои потоки, пусть они работают до тех пор, пока они не закончатся. Попытайтесь минимизировать все зависимости между ними. Особенно при обновлении GUI нет абсолютно никаких оснований ждать завершения этого. Использует ли 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 секунд) будут ли окна-сообщения лучшими?
Если вы отправляете сообщение из каждого потока один раз в секунду или даже более длительный период, то дизайн в порядке. То, что вам не следует делать, - это сообщения сотен или более сообщений на поток в секунду, потому что очередь сообщений Windows имеет конечную длину, а пользовательские сообщения не должны слишком мешать нормальной обработке сообщений (ваша программа начнет казаться невосприимчивой).
где дочерний поток отправляет сообщение Windows (состоящее из записи нескольких строк)
Сообщение окна не может содержать запись. Он содержит два параметра: один из типов WPARAM
, другой тип LPARAM
. Вы можете только указать указатель на такую запись на один из этих типов, поэтому нужно как-то управлять временем жизни записи. Если вы динамически выделяете его, вам также необходимо освободить его, что подвержено ошибкам. Если вы передаете указатель на запись в стеке или в поле объекта, вам нужно убедиться, что он все еще действителен, когда сообщение обрабатывается, что более сложно для сообщений, чем для отправленных сообщений.
Вы предлагаете обернуть код, в который я отправляю сообщение в сетку в блоке TCriticalSection (введите и оставить)? Или мне не нужно беспокоиться о безопасности потоков, так как я пишу в сетку в основном потоке (хотя внутри функции обработчика окна)?
Нет необходимости делать это, так как вызов PostMessage()
будет немедленно возвращаться, поэтому в этот момент не требуется синхронизация. Вам определенно нужно будет беспокоиться о безопасности потоков, к сожалению, вы не можете знать, когда. Вы должны убедиться, что доступ к данным является потокобезопасным, всегда блокируя данные для доступа, используя объекты синхронизации. На самом деле нет способа добиться этого для записей, к данным всегда можно получить доступ напрямую.