记录消息只是其中Synchronize()
根本没有任何意义的那些区域之一。您应该改为创建一个日志目标对象,该对象具有受关键部分保护的字符串列表,并向其添加日志消息。让主VCL线程从该列表中删除日志消息,并在日志窗口中显示它们。这有几个优点:
你不需要拨打Synchronize()
,这是一个坏主意。好的副作用是你的关机问题消失。
工作线程可以继续他们的工作,而不会阻塞主线程事件处理或尝试记录消息的其他线程。
由于可以一次将多条消息添加到日志窗口,因此性能会提高。如果你使用BeginUpdate()
和EndUpdate()
这会加快速度。
我可以看到没有什么缺点 - 日志消息的顺序也被保留下来。
编辑:
我会添加一些更多的信息和一些代码一起玩,为了说明,有更好的方法做你需要做的事情。
调用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
,调用在主线程通知事件,但每秒最多几十倍。
代码会很有帮助,至少在您调用同步和清理违规线程时会很有帮助。 – 2010-03-25 11:40:14