Delphi线程死锁

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,以在主线程中调用通知事件,但每秒最多几十次.


jpf*_*ius 7

考虑替换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线程使用它之前被销毁.正如我所提到的 - 这只是伪代码.

  • 现在后台线程没有等待GUI线程处理消息,您应该为每个新消息创建新的LogStr.逻辑方法是为后台线程中的每个新LogStr分配内存,并在GUI线程中释放相同的内存.因此,您需要做更多的工作才能将您的想法转换为正确的代码. (2认同)