为什么我的多线程应用程序有时会在关闭时挂起?

时间:2010-12-30 01:06:32

标签: multithreading delphi critical-section

我在我的应用程序中使用了几个关键部分。关键部分可防止大数据blob被不同的线程同时修改和访问。

AFAIK它正常工作,除非有时应用程序在退出时挂起。我想知道这是否与我对关键部分的使用有关。

是否有正确的方法可以在析构函数中释放TCriticalSection对象?

感谢所有答案。我正在考虑这些新信息,再次查看我的代码。干杯!

8 个答案:

答案 0 :(得分:7)

正如Rob所说,唯一的要求是确保关键部分当前不属于任何线程。甚至没有关于破坏它的线程。因此,正确销毁TCriticalSection没有任何模式可供遵循。只有您的应用程序必须采取的必要行为才能确保发生。

如果你的申请是锁定的,那么我怀疑这是任何负责任的关键部分的自由。正如MSDN所说(在Rob发布的链接中),DeleteCriticalSection()(最终解放了TCriticalSection调用)不会阻止任何线程。

如果您正在释放其他线程仍在尝试访问的关键部分,那么您将获得访问冲突和其他意外行为,而不是死锁,因为这个小代码示例应该可以帮助您演示:

implementation

uses
  syncobjs;


  type
    tworker = class(tthread)
    protected
      procedure Execute; override;
    end;


  var
    cs: TCriticalSection;
    worker: Tworker;


procedure TForm2.FormCreate(Sender: TObject);
begin
  cs := TCriticalSection.Create;

  worker := tworker.Create(true);
  worker.FreeOnTerminate := TRUE;
  worker.Start;

  sleep(5000);

  cs.Enter;

  showmessage('will AV before you see this');
end;

{ tworker }

procedure tworker.Execute;
begin
  inherited;
  cs.Free;
end;

添加到表单的实现单元,根据需要更正FormCreate()事件处理程序的“TForm2”引用。

在FormCreate()中,这会创建一个关键部分,然后启动一个线程,其唯一目的是释放该部分。我们引入Sleep()延迟来给线程初始化和执行时间,然后我们尝试自己进入临界区。

我们当然不能因为它是免费的。但是我们的代码并没有挂起 - 它没有死锁,试图访问其他东西所拥有的资源,它只是爆炸了,因为我们正在尝试访问不再存在的资源。

你可以更加确定在这种情况下通过NIL在免费提供关键部分参考时创建一个AV。

现在,尝试将FormCreate()代码更改为:

  cs := TCriticalSection.Create;

  worker := tworker.Create(true);
  worker.FreeOnTerminate := TRUE;

  cs.Enter;
  worker.Start;

  sleep(5000);

  cs.Leave;

  showmessage('appearances can be deceptive');

这改变了一些事情......现在主线程将获得关键部分的所有权 - 工作线程现在将释放临界区,而它仍然由主线程拥有。

但是,在这种情况下,对cs.Leave的调用不一定会导致访问冲突。在这种情况下(afaict)发生的所有情况都是允许拥有的线程按照预期“离开”该部分(当然不是,因为该部分已经消失,但它似乎到它离开之前输入的部分的线程... ...

...在更复杂的情况下,可能会发生访问冲突或其他错误,因为以前用于临界区对象的内存可能会在您调用它的Leave()方法时重新分配给其他对象,导致某些其他未知对象的调用或访问无效内存等。

再次,更改worker.Execute()以便在释放之后NIL是关键部分ref将确保在尝试调用cs.Leave()时发生访问冲突,因为Leave()调用Release()和Release()是虚拟的 - 调用带有NIL引用的虚方法保证AV(对于调用虚拟Acquire()方法的Enter()同样如此。)

无论如何:

最坏情况:异常或奇怪的行为

“最佳”案例:拥有线程似乎认为它已正常“离开”该部分。

在任何一种情况下都不会发生死锁或挂起,只是因为当一个线程中的临界区域被释放时,其他线程随后会尝试进入或离开该临界区域。

所有这些都是一种圆润的说法,听起来你的线程代码中有一个更基本的竞争条件,与你的关键部分的自由关系没有直接关系。

无论如何,我希望我的一些调查工作可能会让你走上正确的道路。

答案 1 :(得分:2)

确保没有任何东西仍拥有关键部分。否则,MSDN explains,“等待已删除的关键部分的所有权的线程的状态是未定义的。”除此之外,请像对待所有其他对象一样在其上调用Free

答案 2 :(得分:1)

  

AFAIK它正常工作,除非有时应用程序在退出时挂起。我想知道这是否与我对关键部分的使用有关。

是的。但问题可能不在于破坏。你可能有僵局。

死锁是指两个线程在两个独占资源上等待,每个资源都需要两个独立资源,每个资源只拥有一个:

//Thread1:
FooLock.Enter;
BarLock.Enter;

//Thread2:
BarLock.Enter;
FooLock.Enter;

对抗这些的方法是订购你的锁。如果某个线程需要其中两个,则必须按特定顺序输入它们:

//Thread1:
FooLock.Enter;
BarLock.Enter;

//Thread2:
FooLock.Enter;
BarLock.Enter;

这样就不会发生死锁。

许多事情都可能触发死锁,而不仅仅是两个关键部分。例如,您可能使用过SendMessage(同步消息调度)或Delphi的Synchronize AND一个关键部分:

//Thread1:
OnPaint:
  FooLock.Enter;
  FooLock.Leave;

//Thread2:
FooLock.Enter;
Synchronize(SomeProc);
FooLock.Leave;

Synchronize和SendMessage向Thread1发送消息。为了发送这些消息,Thread1需要完成它正在做的任何工作。例如,OnPaint处理程序。

但是要完成绘画,它需要FooLock,它由Thread2拍摄,等待Thread1完成绘画。死锁。

解决这个问题的方法是永远不要使用Synchronize和SendMessage(最好的方法),或者至少在任何锁之外使用它们。

  

是否有正确的方法可以在析构函数中释放TCriticalSection对象?

在析构函数中是否释放TCriticalSection并不重要。

但是在释放TCriticalSection之前,你必须确保所有可以使用它的线程都被停止或者处于他们不能再尝试进入这一部分的状态。

例如,如果您的线程在分派网络消息时进入此部分,则必须确保断开网络并处理所有待处理的消息。

在大多数情况下,未能执行此操作会触发访问冲突,有时甚至没有(如果您很幸运),并且很少会出现死锁。

答案 3 :(得分:1)

使用TCriticalSection以及关键部分本身并不神奇。尝试用普通的API调用替换TCriticalSection对象:

uses
  Windows, ...

var
  CS: TRTLCriticalSection;

...

EnterCriticalSection(CS);
....
here goes your code that you have to protect from access by multiple threads simultaneously
...
LeaveCriticalSection(FCS);
...

initialization
  InitializeCriticalSection(CS);

finalization
  DeleteCriticalSection(CS);

切换到API不会损害代码的清晰度,但也许有助于揭示隐藏的错误。

答案 4 :(得分:0)

您需要使用try..finally阻止保护所有关键部分。

使用TRTLCriticalSection而不是TCriticalSection类。它是跨平台的,而TCriticalSection只是一个不必要的包装器。

如果在数据处理过程中发生任何异常,则不会留下批评部分,而另一个线程可能会阻止。

如果您想要快速响应,您还可以将TryEnterCriticalSection用于某些用户界面流程等。

以下是一些良好的练习规则:

  1. 使您的TRTLCriticalSection成为Class的属性;
  2. 在类构造函数中调用InitializeCriticalSection,然后在类析构函数中调用DeleteCriticalSection;
  3. 使用EnterCriticalSection()...尝试......做点什么......最后LeaveCriticalSection();端;
  4. 以下是一些代码示例:

    type
      TDataClass = class
      protected
        fLock: TRTLCriticalSection;
      public
        constructor Create;
        destructor Destroy; override;
        procedure SomeDataProcess;
      end;
    
    constructor TDataClass.Create;
    begin
      inherited;
      InitializeCriticalSection(fLock);
    end;
    
    destructor TDataClass.Destroy;
    begin
      DeleteCriticalSection(fLock);
      inherited;
    end;
    
    procedure TDataClass.SomeDataProcess;
    begin
      EnterCriticalSection(fLock);
      try
        // some data process
      finally
        LeaveCriticalSection(fLock);
      end;
    end;
    

答案 5 :(得分:0)

如果您应用中的唯一显式同步代码是通过关键部分,那么跟踪它应该不会太难。

您表示您仅在终止时看到了死锁。当然这并不意味着它不能在您的应用程序的正常操作期间发生,但我的猜测(我们必须猜测没有更多信息)是它是一个重要的线索。

我认为错误可能与线程被强制终止的方式有关。如果一个线程在仍然持有锁的情况下终止,那么你所描述的死锁就会发生,但是另一个线程在它有机会终止之前就试图获取锁。

一个非常简单的事情可以立即解决问题,就像其他人正确说的那样,确保锁的所有用途都受到Try / Finally的保护。这确实是一个关键点。

Delphi中有两种主要的资源生命周期管理模式,如下所示:

lock.Acquire;
Try
  DoSomething();
Finally
  lock.Release;
End;

另一个主要模式是在Create / Destroy中配对获取/释放,但在锁的情况下这种情况就不那么常见了。

假设你的锁的使用模式是我怀疑的(即在同一方法中获取和释放),你能否确认所有用途都受Try / Finally保护?

答案 6 :(得分:0)

如果您的应用程序仅在退出时挂起/死锁,请检查所有线程的onterminate事件。如果主线程发信号通知其他线程终止,然后在释放它们之前等待它们。重要的是不要在on terminate事件中进行任何同步调用。当主线程等待工作线程终止时,这可能导致死锁。但是同步调用正在主线程上等待。

答案 7 :(得分:0)

不要删除对象析构函数中的关键部分。有时会导致您的应用程序崩溃。

使用单独的方法删除关键部分。

程序someobject.deleteCritical();
开始
  DeleteCriticalSection(CriticalSection的);
结束;

析构函数someobject.destroy();
开始
  //你的释放任务在这里 端;

1)你打电话给删除关键部分
2)释放(释放)对象后