Delphi线程死锁

时间:2010-03-25 11:35:24

标签: delphi multithreading

我在解决某些线程时有时遇到死锁问题。我试图调试问题但是在IDE中调试时似乎永远不存在死锁,可能是因为IDE中事件的速度很慢。

问题:

主线程在应用程序启动时创建多个线程。线程始终处于活动状态并与主线程同步。没问题。当应用程序结束时,线程被销毁(mainform.onclose):

thread1.terminate;
thread1.waitfor;
thread1.free;

等等。

但有时其中一个线程(使用synchronize将一些字符串记录到备忘录中)将在关闭时锁定整个应用程序。我怀疑当我调用waitform并且harmaggeddon发生时线程正在同步,但这只是猜测,因为在调试时永远不会发生死锁(或者我从来没有能够重现它)。有什么建议吗?

5 个答案:

答案 0 :(得分:28)

记录消息只是Synchronize()完全没有任何意义的区域之一。您应该创建一个日志目标对象,该对象具有一个受关键部分保护的字符串列表,并向其添加日志消息。让主VCL线程从该列表中删除日志消息,并在日志窗口中显示它们。这有几个好处:

  • 您无需致电Synchronize(),这只是一个坏主意。好的副作用是你的关机问题消失了。

  • 工作线程可以继续工作而不会阻塞主线程事件处理,或者尝试记录消息的其他线程。

  • 性能提升,因为可以一次性将多条消息添加到日志窗口中。如果您使用BeginUpdate()EndUpdate(),这会加快速度。

我可以看到没有缺点 - 日志消息的顺序也会保留。

修改

我将添加更多信息和一些代码,以说明有更好的方法来完成您需要做的事情。

从VCL程序中的主应用程序线程以外的其他线程调用Synchronize()将导致调用线程阻塞,传递的代码将在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,以在主线程中调用通知事件,但最多每秒几十次。

答案 1 :(得分: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));

这可以避免两个线程等待彼此的所有死锁问题。

编辑(感谢Serg的提示):请注意,以所描述的方式传递字符串是不安全的,因为字符串可能会在VCL线程使用它之前被销毁。正如我所提到的 - 这只是伪代码。

答案 2 :(得分:2)

将互斥对象添加到主线程。尝试关闭表单时获取互斥锁。在其他线程中检查互斥锁,然后在处理序列中进行同步。

答案 3 :(得分:1)

很简单:

TMyThread = class(TThread)
protected
  FIsIdle: boolean; 
  procedure Execute; override;
  procedure MyMethod;
public
  property IsIdle : boolean read FIsIdle write FIsIdle; //you should use critical section to read/write it
end;

procedure TMyThread.Execute;
begin
  try
    while not Terminated do
    begin
      Synchronize(MyMethod);
      Sleep(100);
    end;
  finally
    IsIdle := true;
  end;
end;

//thread destroy;
lMyThread.Terminate;
while not lMyThread.IsIdle do
begin
  CheckSynchronize;
  Sleep(50);
end;

答案 4 :(得分:0)

Delphi的TThread对象(和继承类)在销毁时已经调用了WaitFor,但这取决于你是否使用CreateSuspended创建了线程。如果在调用第一个Resume之前使用CreateSuspended = true来执行额外的初始化,则应考虑创建自己的构造函数(调用inherited Create(false);)来执行额外的初始化。