Ответ 1
Запись сообщений - это только одна из тех областей, где Synchronize()
не имеет никакого смысла. Вместо этого вы должны создать целевой объект журнала, который имеет список строк, защищенный критическим разделом, и добавить к нему свои сообщения журнала. Попросите основной поток VCL удалить сообщения журнала из этого списка и показать их в окне журнала. Это имеет несколько преимуществ:
-
Вам не нужно вызывать
Synchronize()
, что является просто плохой идеей. Хороший побочный эффект заключается в том, что ваши проблемы с остановкой исчезают. -
Рабочие потоки могут продолжать свою работу без блокировки обработки событий основного потока или других потоков, пытающихся зарегистрировать сообщение.
-
Производительность увеличивается, так как несколько сообщений могут быть добавлены в окно журнала за один раз. Если вы используете
BeginUpdate()
иEndUpdate()
, это ускорит процесс.
Нет никаких недостатков, которые я вижу - порядок сообщений журнала также сохраняется.
Edit:
Я добавлю дополнительную информацию и немного кода для игры, чтобы проиллюстрировать, что есть намного лучшие способы сделать то, что вам нужно сделать.
Вызов Synchronize()
из другого потока, кроме основного потока приложений в программе VCL, приведет к блокировке вызывающего потока, переданный код будет выполняться в контексте потока VCL, а затем вызывающий поток будет разблокирован и продолжать работать. Это может быть хорошей идеей во времена однопроцессорных машин, на которых в любом случае может работать только один поток за один раз, но с несколькими процессорами или ядрами это гигантские отходы, и их следует избегать любой ценой. Если у вас 8 рабочих потоков на 8-ядерном компьютере, их вызов Synchronize()
, вероятно, ограничит пропускную способность до некоторой степени возможной.
На самом деле вызов Synchronize()
никогда не был хорошей идеей, так как это может привести к взаимоблокировкам. Еще одна убедительная причина не использовать его, когда-либо.
Использование PostMessage()
для отправки сообщений журнала будет заботиться о проблеме взаимоблокировки, но у нее есть свои проблемы:
-
Каждая строка журнала приведет к тому, что сообщение будет опубликовано и обработано, что приведет к большим накладным расходам. Невозможно обработать несколько сообщений журнала за один раз.
-
Сообщения Windows могут содержать только данные размера машинного слова в параметрах. Поэтому отправка строк невозможна. Отправка строк после приведения типа
PChar
небезопасна, так как строка может быть освобождена к моменту обработки сообщения. Выделение памяти в рабочем потоке и освобождение этой памяти в потоке VCL после обработки сообщения является выходом. Способ, который добавляет еще больше накладных расходов. -
Очереди сообщений в Windows имеют конечный размер. Проводка слишком большого количества сообщений может привести к тому, что очередь будет заполнена, а сообщения будут удалены. Это не очень хорошо, и вместе с предыдущим пунктом это приводит к утечкам памяти.
-
Все сообщения в очереди будут обработаны до того, как будут созданы какие-либо сообщения таймера или краски. Таким образом, постоянный поток многих опубликованных сообщений может привести к тому, что программа перестанет реагировать.
Структура данных, которая собирает сообщения журнала, может выглядеть так:
type
TLogTarget = class(TObject)
private
fCritSect: TCriticalSection;
fMsgs: TStrings;
public
constructor Create;
destructor Destroy; override;
procedure GetLoggedMsgs(AMsgs: TStrings);
procedure LogMessage(const AMsg: string);
end;
constructor TLogTarget.Create;
begin
inherited;
fCritSect := TCriticalSection.Create;
fMsgs := TStringList.Create;
end;
destructor TLogTarget.Destroy;
begin
fMsgs.Free;
fCritSect.Free;
inherited;
end;
procedure TLogTarget.GetLoggedMsgs(AMsgs: TStrings);
begin
if AMsgs <> nil then begin
fCritSect.Enter;
try
AMsgs.Assign(fMsgs);
fMsgs.Clear;
finally
fCritSect.Leave;
end;
end;
end;
procedure TLogTarget.LogMessage(const AMsg: string);
begin
fCritSect.Enter;
try
fMsgs.Add(AMsg);
finally
fCritSect.Leave;
end;
end;
Многие потоки могут одновременно вызывать LogMessage()
, ввод критического раздела будет сериализовывать доступ к списку, а после добавления их сообщений потоки могут продолжать свою работу.
Это оставляет вопрос о том, как поток VCL знает, когда вызывать GetLoggedMsgs()
, чтобы удалить сообщения из объекта и добавить их в окно. Версия для бедных - это таймер и опрос. Лучшим способом было бы вызвать PostMessage()
при добавлении сообщения журнала:
procedure TLogTarget.LogMessage(const AMsg: string);
begin
fCritSect.Enter;
try
fMsgs.Add(AMsg);
PostMessage(fNotificationHandle, WM_USER, 0, 0);
finally
fCritSect.Leave;
end;
end;
У этой проблемы еще слишком много сообщений. Сообщение должно быть отправлено только после обработки предыдущего:
procedure TLogTarget.LogMessage(const AMsg: string);
begin
fCritSect.Enter;
try
fMsgs.Add(AMsg);
if InterlockedExchange(fMessagePosted, 1) = 0 then
PostMessage(fNotificationHandle, WM_USER, 0, 0);
finally
fCritSect.Leave;
end;
end;
Тем не менее, это может быть улучшено. Использование таймера решает проблему отправки сообщений, заполняющих очередь. Ниже приведен небольшой класс, который реализует это:
type
TMainThreadNotification = class(TObject)
private
fNotificationMsg: Cardinal;
fNotificationRequest: integer;
fNotificationWnd: HWND;
fOnNotify: TNotifyEvent;
procedure DoNotify;
procedure NotificationWndMethod(var AMsg: TMessage);
public
constructor Create;
destructor Destroy; override;
procedure RequestNotification;
public
property OnNotify: TNotifyEvent read fOnNotify write fOnNotify;
end;
constructor TMainThreadNotification.Create;
begin
inherited Create;
fNotificationMsg := RegisterWindowMessage('thrd_notification_msg');
fNotificationRequest := -1;
fNotificationWnd := AllocateHWnd(NotificationWndMethod);
end;
destructor TMainThreadNotification.Destroy;
begin
if IsWindow(fNotificationWnd) then
DeallocateHWnd(fNotificationWnd);
inherited Destroy;
end;
procedure TMainThreadNotification.DoNotify;
begin
if Assigned(fOnNotify) then
fOnNotify(Self);
end;
procedure TMainThreadNotification.NotificationWndMethod(var AMsg: TMessage);
begin
if AMsg.Msg = fNotificationMsg then begin
SetTimer(fNotificationWnd, 42, 10, nil);
// set to 0, so no new message will be posted
InterlockedExchange(fNotificationRequest, 0);
DoNotify;
AMsg.Result := 1;
end else if AMsg.Msg = WM_TIMER then begin
if InterlockedExchange(fNotificationRequest, 0) = 0 then begin
// set to -1, so new message can be posted
InterlockedExchange(fNotificationRequest, -1);
// and kill timer
KillTimer(fNotificationWnd, 42);
end else begin
// new notifications have been requested - keep timer enabled
DoNotify;
end;
AMsg.Result := 1;
end else begin
with AMsg do
Result := DefWindowProc(fNotificationWnd, Msg, WParam, LParam);
end;
end;
procedure TMainThreadNotification.RequestNotification;
begin
if IsWindow(fNotificationWnd) then begin
if InterlockedIncrement(fNotificationRequest) = 0 then
PostMessage(fNotificationWnd, fNotificationMsg, 0, 0);
end;
end;
Экземпляр класса можно добавить в TLogTarget
, чтобы вызвать событие уведомления в основном потоке, но не более нескольких десятков раз в секунду.