1

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

AFAIK 一切正常,除了有时应用程序在退出时挂起。我想知道这是否与我使用关键部分有关。

有没有正确的方法来释放析构函数中的 TCriticalSection 对象?

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

4

8 回答 8

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 的调用不一定会导致访问冲突。在这种情况下发生的所有事情(afact)是允许拥有的线程按预期“离开”该部分(当然不是,因为该部分已经消失,但在线程看来它有离开了之前进入的部分)...

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

同样,更改 worker.Execute() 使其在释放后成为 NIL 的关键部分引用,这将确保尝试调用 cs.Leave() 时发生访问冲突,因为 Leave() 调用 Release() 和 Release()是虚拟的 - 使用 NIL 引用调用虚拟方法可以保证 AV(Enter() 同上,它调用虚拟 Acquire() 方法)。

在任何情况下:

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

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

在这两种情况下,死锁或挂起都不会仅仅因为一个线程中的临界区被释放而其他线程尝试进入或离开该临界区的结果。

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

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

于 2010-12-30T03:44:41.143 回答
2

只要确保没有任何东西仍然拥有关键部分。否则,MSDN 解释说,“等待已删除临界区所有权的线程状态未定义。” 除此之外,Free像处理所有其他对象一样调用它。

于 2010-12-30T02:33:55.230 回答
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,它由等待Thread1完成绘画的Thread2获取。僵局。

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

有没有正确的方法来释放析构函数中的 TCriticalSection 对象?

无论您在哪里释放 TCriticalSection,在析构函数中与否都无关紧要。

但在释放 TCriticalSection 之前,您必须确保所有可能使用它的线程都已停止或处于无法再尝试进入此部分的状态。

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

如果不这样做,在大多数情况下会触发访问冲突,有时什么都不会(如果你幸运的话),并且很少会出现死锁。

于 2010-12-30T08:11:09.960 回答
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 不会损害代码的清晰度,但可能有助于揭示隐藏的错误。

于 2010-12-30T08:34:03.063 回答
0

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

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

我假设该错误可能与线程被强制终止的方式有关。如果一个线程在仍然持有锁的情况下终止,但另一个线程在它有机会终止之前尝试获取锁,则会发生如您所描述的死锁。

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

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

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

另一个主要模式是在 Create/Destroy 中配对获取/释放,但在锁定的情况下这种情况要少得多。

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

于 2010-12-30T10:47:11.710 回答
0

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

于 2011-01-03T00:41:30.577 回答
0

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

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

如果在数据处理过程中出现异常,则不离开关键部分,可能会阻塞另一个线程。

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

以下是一些良好的实践规则:

  1. 使您的 TRTLCriticalSection 成为类的属性;
  2. 在类构造函数中调用 InitializeCriticalSection,然后在类析构函数中调用 DeleteCriticalSection;
  3. 使用 EnterCriticalSection()... 尝试... 做某事... 最后 LeaveCriticalSection(); 结尾;

这是一些代码示例:

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;
于 2010-12-30T09:52:46.380 回答
0

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

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

过程 someobject.deleteCritical();
开始
DeleteCriticalSection(criticalSection);
结尾;

析构函数 someobject.destroy();
begin // 在这里 结束
你的发布任务;

1)您调用删除临界区
2)释放(释放)对象后

于 2013-04-28T23:20:17.637 回答