Delphi线程死锁 [英] Delphi threads deadlock

查看:166
本文介绍了Delphi线程死锁的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有时会在销毁某些线程时遇到死锁的问题。我试图调试问题,但在IDE中调试时,死锁似乎并不存在,也许是因为IDE中的事件速度很慢。

I am having a problem sometimes with a deadlock when destroying some threads. I've tried to debug the problem but the deadlock never seems to exist when debugging in the IDE, perhaps because of the low speed of the events in the IDE.

问题:

主线程在应用程序启动时创建多个线程。线程总是存活并与主线程同步。没有问题。当应用程序结束(mainform.onclose)时,线程将被破坏:

The main thread creates several threads when the application starts. The threads are always alive and synchronizing with the main thread. No problems at all. The threads are destroyed when the application ends (mainform.onclose) like this:

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

等等。

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

But sometimes one of the threads (which logs some string to a memo, using synchronize) will lock the whole application when closing. I suspect that the thread is synchronizing when I call waitform and the harmaggeddon happens, but that's is just a guess because the deadlock never happens when debugging (or I've never been able to reproduce it anyway). Any advice?

推荐答案

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

Logging messages is just one of those areas where Synchronize() doesn't make any sense at all. You should instead create a log target object, which has a string list, protected by a critical section, and add your log messages to it. Have the main VCL thread remove the log messages from that list, and show them in the log window. This has several advantages:


  • 您不需要调用 Synchronize(),这只是一个坏主意。好的副作用是你的这种停机问题消失了。

  • You don't need to call Synchronize(), which is just a bad idea. Nice side effect is that your kind of shutdown problems disappear.

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

Worker threads can continue with their work without blocking on the main thread event handling, or on other threads trying to log a message.

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

Performance increases, as multiple messages can be added to the log window in one go. If you use BeginUpdate() and EndUpdate() this will speed things up.

没有任何缺点,我可以看到 - 日志消息的顺序也保留下来。

There are no disadvantages that I can see - the order of log messages is preserved as well.

编辑:

我将添加一些更多的信息和一些代码来玩,为了说明有很多

I will add some more information and a bit of code to play with, in order to illustrate that there are much better ways to do what you need to do.

从与主应用程序不同的线程调用 Synchronize() VCL程序中的线程将导致调用线程阻塞,所传递的代码将在VCL线程的上下文中执行,然后调用线程将被解除阻塞并继续运行。在单处理器机器的时代,这可能是一个好主意,只有一个线程可以一次运行,但是使用多个处理器或内核,这是一个巨大的浪费,应该不惜一切代价避免。如果您在8核心机器上有8个工作线程,请调用 Synchronize()可能会将吞吐量限制在可能的一小部分。

Calling Synchronize() from a different thread than the main application thread in a VCL program will cause the calling thread to block, the passed code to be executed in the context of the VCL thread, and then the calling thread will be unblocked and continue to run. That may have been a good idea in the times of single processor machines, on which only one thread can run at a time anyway, but with multiple processors or cores it's a giant waste and should be avoided at all costs. If you have 8 worker threads on an 8 core machine, having them call Synchronize() will probably limit the throughput to a fraction of what's possible.

实际上,调用 Synchronize()从来不是一个好主意,因为它可能导致死锁。使用 PostMessage()发送日志消息将会占用一个更有说服力的原因,不要使用它。

Actually, calling Synchronize() was never a good idea, as it can lead to deadlocks. One more convincing reason to not use it, ever.

关心死锁问题,但它有自己的问题:

Using PostMessage() to send the log messages will take care of the deadlock issue, but it has its own problems:


  • 每个日志字符串将导致一个消息被发布和处理,造成很多开销。没有办法一次处理几个日志消息。

  • Each log string will cause a message to be posted and processed, causing much overhead. There is no way to handle several log messages in one go.

Windows消息只能在参数中携带机器字大小的数据。因此发送字符串是不可能的。在类型转换之后发送字符串到 PChar 是不安全的,因为字符串可能在消息被处理时被释放。在处理消息后,在工作线程中分配内存并释放VCL线程中的内存是一种出路。一种增加更多开销的方式。

Windows messages can only carry machine-word sized data in parameters. Sending strings is therefore impossible. Sending strings after a typecast to PChar is unsafe, as the string may have been freed by the time the message is processed. Allocating memory in the worker thread and freeing that memory in the VCL thread after the message has been processed is a way out. A way that adds even more overhead.

Windows中的消息队列具有有限的大小。发布太多邮件可能导致队列变满,邮件被删除。这不是一件好事,并且与之前的观点一起导致内存泄漏。

The message queues in Windows have a finite size. Posting too many messages can lead to the queue to become full and messages being dropped. That's not a good thing, and together with the previous point it leads to memory leaks.

队列中的所有消息将在任何计时器或绘画消息之前被处理将被生成。因此,许多发布消息的稳定流可能导致程序无响应。

All messages in the queue will be processed before any timer or paint messages will be generated. A steady stream of many posted messages can therefore cause the program to become unresponsive.

收集日志消息的数据结构可能看起来像这样:

A data structure that collects log messages could look like this:

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()进入关键部分将序列化对列表的访问,在添加他们的消息之后,线程可以继续他们的工作。

Many threads can call LogMessage() concurrently, entering the critical section will serialize access to the list, and after adding their message the threads can continue with their work.

这就是VCL线程知道什么时候调用 GetLoggedMsgs()从对象中删除消息并将其添加到窗口中。一个穷人的版本将是一个定时器和民意调查。当添加日志消息时,更好的方法是调用 PostMessage()

That leaves the question how the VCL thread knows when to call GetLoggedMsgs() to remove the messages from the object and add them to the window. A poor man's version would be to have a timer and poll. A better way would be to call PostMessage() when a log message is added:

procedure TLogTarget.LogMessage(const AMsg: string);
begin
  fCritSect.Enter;
  try
    fMsgs.Add(AMsg);
    PostMessage(fNotificationHandle, WM_USER, 0, 0);
  finally
    fCritSect.Leave;
  end;
end;

这仍然有太多发布的消息的问题。只有当前一个消息处理完毕时,才需要发布消息:

This still has the problem with too many posted messages. A message needs only be posted when the previous one has been processed:

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;

尽管如此,仍然可以改进。使用定时器解决了发布的消息填满队列的问题。以下是实现这一点的小类:

That still can be improved, though. Using a timer solves the problem of the posted messages filling up the queue. The following is a small class that implements this:

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

An instance of the class can be added to TLogTarget, to call a notification event in the main thread, but at most a few dozen times per second.

这篇关于Delphi线程死锁的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆