Lob*_*uno 14 delphi multithreading
在销毁某些线程时,我遇到了有时遇到死锁的问题.我试图调试问题但是在IDE中调试时似乎永远不存在死锁,可能是因为IDE中事件的速度很慢.
问题:
主线程在应用程序启动时创建多个线程.线程始终处于活动状态并与主线程同步.没问题.当应用程序结束时,线程被销毁(mainform.onclose),如下所示:
thread1.terminate;
thread1.waitfor;
thread1.free;
Run Code Online (Sandbox Code Playgroud)
等等.
但有时其中一个线程(使用synchronize将一些字符串记录到备忘录中)将在关闭时锁定整个应用程序.我怀疑当我调用waitform并且harmaggeddon发生时线程正在同步,但这只是猜测,因为在调试时永远不会发生死锁(或者我从来没有能够重现它).有什么建议?
mgh*_*hie 29
记录消息只是其中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;
Run Code Online (Sandbox Code Playgroud)
许多线程可以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;
Run Code Online (Sandbox Code Playgroud)
这仍然存在过多发布消息的问题.只有在处理完上一条消息后才需要发布消息:
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;
Run Code Online (Sandbox Code Playgroud)
但是,这仍然可以改进.使用计时器解决了发布消息填满队列的问题.以下是实现此目的的小类:
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;
Run Code Online (Sandbox Code Playgroud)
可以添加类的实例TLogTarget
,以在主线程中调用通知事件,但每秒最多几十次.
考虑替换Synchronize
调用PostMessage
并在表单中处理此消息,以向备忘录添加日志消息.类似的东西:(把它作为伪代码)
WM_LOG = WM_USER + 1;
...
MyForm = class (TForm)
procedure LogHandler (var Msg : Tmessage); message WM_LOG;
end;
...
PostMessage (Application.MainForm.Handle, WM_LOG, 0, PChar (LogStr));
Run Code Online (Sandbox Code Playgroud)
这避免了两个线程等待彼此的所有死锁问题.
编辑(感谢Serg的提示):请注意,以所描述的方式传递字符串是不安全的,因为字符串可能在VCL线程使用它之前被销毁.正如我所提到的 - 这只是伪代码.
归档时间: |
|
查看次数: |
5097 次 |
最近记录: |