为什么我的多线程应用程序有时会在关闭时挂起?
我在我的应用程序中使用了几个关键部分。临界区可防止不同线程同时修改和访问大型数据块。
据我所知,除了有时应用程序在退出时挂起之外,一切都正常工作。我想知道这是否与我使用关键部分有关。
是否有在析构函数中释放 TCriticalSection 对象的正确方法?
感谢您的所有回答。我正在再次查看我的代码并牢记这些新信息。干杯!
I'm using several critical sections in my application. The critical sections prevent large data blobs from being modified and accessed simultaneously by different threads.
AFAIK it's all working correctly except sometimes the application hangs when exiting. I'm wondering if this is related to my use of critical sections.
Is there a correct way to free TCriticalSection objects in a destructor?
Thanks for all the answers. I'm looking over my code again with this new information in mind. Cheers!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
正如 Rob 所说,唯一的要求是确保关键部分当前不属于任何线程。甚至连即将摧毁它的线程也没有。因此,正确销毁 TCriticalSection 没有可遵循的模式。仅您的应用程序必须采取措施以确保发生所需的行为。
如果您的应用程序被锁定,那么我怀疑是否释放了任何负责的关键部分。正如 MSDN 所说(在 Rob 发布的链接中),DeleteCriticalSection()(这最终是释放 TCriticalSection 所调用的)不会阻塞任何线程。
如果您释放其他线程仍在尝试访问的关键部分,您将遇到访问冲突和其他意外行为,而不是死锁,因为这个小代码示例应该可以帮助您演示:
添加到表单的实现单元,更正根据需要,FormCreate() 事件处理程序的“TForm2”引用。
在 FormCreate() 中,这会创建一个关键部分,然后启动一个线程,其唯一目的是释放该部分。我们引入一个 Sleep() 延迟来给线程时间来初始化和执行,然后我们尝试自己进入临界区。
我们当然不能,因为它已经被释放了。但是我们的代码并没有挂起 - 它在尝试访问其他资源所拥有的资源时并没有死锁,它只是崩溃了,因为我们正在尝试访问不再存在的资源。
在这种情况下,通过在关键部分引用被释放时对它进行 NIL 操作,您可以更加确定地创建 AV。
现在,尝试将 FormCreate() 代码更改为:
这会改变一些事情...现在主线程将获得关键部分的所有权 - 工作线程现在将释放关键部分,同时它仍由主线程拥有。
但在这种情况下,对 cs.Leave 的调用不一定会导致访问冲突。在这种情况下(afaict)发生的所有事情是允许拥有线程按照预期“离开”该部分(当然不是,因为该部分已经消失,但似乎 到它已离开之前进入的部分的线程) ...
...在更复杂的情况下,可能会出现访问冲突或其他错误,因为之前用于关键部分对象的内存可能会重新分配给当你调用它的 Leave() 方法时,它会调用一些其他对象,从而导致调用一些其他未知对象或访问无效内存等。
再次,更改worker.Execute(),使其成为释放后的关键部分引用'它将确保尝试调用 cs.Leave() 时发生访问冲突,因为 Leave() 调用 Release() 且 Release() 是虚拟的 - 调用带有 NIL 引用的虚拟方法可以保证 AV(Enter( 同上) ),它调用虚拟 Acquire() 方法)。
无论如何:
最坏的情况:异常或奇怪的行为
“最好”的情况:所属线程似乎认为它已正常“离开”该部分。
在这两种情况下,死锁或挂起都不会仅仅因为一个线程中的关键部分被释放而其他线程尝试进入或离开该关键部分而发生。
所有这些都是一种迂回的说法,听起来你的线程代码中有一个更基本的竞争条件,与关键部分的释放没有直接关系。
无论如何,我希望我的一点点调查工作能让你走上正确的道路。
As Rob says, the only requirement is that you ensure that the critical section is not currently owned by any thread. Not even the thread about to destroy it. So there is no pattern to follow for correctly destroying a TCriticalSection, as such. Only a required behaviour that your application must take steps to ensure occurs.
If your application is locking then I doubt it is the free'ing of any critical section that is responsible. As MSDN says (in the link that Rob posted), the DeleteCriticalSection() (which is ultimately what free'ing a TCriticalSection calls) does not block any threads.
If you were free'ing a critical section that other threads were still trying to access you would get access violations and other unexpected behaviours, not deadlocks, as this little code sample should help you demonstrate:
Add to the implementation unit of a form, correcting the "TForm2" reference for the FormCreate() event handler as required.
In FormCreate() this creates a critical section then launches a thread whose sole purpose is to free that section. We introduce a Sleep() delay to give the thread time to initialise and execute, then we try to enter the critical section ourselves.
We can't of course because it has been free'd. But our code doesn't hang - it is not deadlocked trying to access a resource that is owned by something else, it simply blows up because, well, we're trying to access a resource that no longer exists.
You could be even more sure of creating an AV in this scenario by NIL'ing the critical section reference when it is free'd.
Now, try changing the FormCreate() code to this:
This changes things... now the main thread will take ownership of the critical section - the worker thread will now free the critical section while it is still owned by the main thread.
In this case however, the call to cs.Leave does not necessarily cause an access violation. All that occurs in this scenario (afaict) is that the owning thread is allowed to "leave" the section as it would expect to (it doesn't of course, because the section has gone, but it seems to the thread that it has left the section it previously entered) ...
... in more complex scenarios an access violation or other error is possibly likely, as the memory previously used for the critical section object may be re-allocated to some other object by the time you call it's Leave() method, resulting in some call to some other unknown object or access to invalid memory etc.
Again, changing the worker.Execute() so that it NIL's the critical section ref after free'ing it would ensure an access violation on the attempt to call cs.Leave(), since Leave() calls Release() and Release() is virtual - calling a virtual method with a NIL reference is guaranteed to AV (ditto for Enter() which calls the virtual Acquire() method).
In any event:
Worst case: an exception or weird behaviour
"Best" case: the owning thread appears to believe it has "left" the section as normal.
In neither case is a deadlock or a hang going to occur simply as the result of when a critical section is free'd in one thread in relation to when other threads then try to enter or leave that critical section.
All of which is a round-a-bout way of saying that it sounds like you have a more fundamental race condition in your threading code not directly related to the free'ing of your critical sections.
In any event, I hope my little bit of investigative work might set you down the right path.
只要确保没有任何东西仍然拥有该临界区即可。否则,MSDN 解释,“等待所有权的线程的状态删除的关键部分是未定义的。”除此之外,像对待所有其他对象一样,对其调用
Free
。Just make sure nothing still owns the critical section. Otherwise, MSDN explains, "the state of the threads waiting for ownership of the deleted critical section is undefined." Other than that, call
Free
on it like you do with all other objects.是的。但问题可能不在于破坏。你可能遇到了僵局。
死锁是指两个线程等待两个独占资源,每个线程都想要这两个资源,但每个线程只拥有一个资源:
解决这些问题的方法是对锁进行排序。如果某个线程想要其中两个,则必须仅按特定顺序输入它们:
这样就不会发生死锁。
很多事情都会引发死锁,不仅仅是两个关键部分。例如,您可能使用过 SendMessage(同步消息调度)或 Delphi 的 Synchronize AND 一个关键部分:
Synchronize 和 SendMessage 将消息发送到 Thread1。为了分派这些消息,Thread1 需要完成它正在做的任何工作。例如,OnPaint 处理程序。
但要完成绘画,它需要 FooLock,该 FooLock 由等待 Thread1 完成绘画的 Thread2 获取。僵局。
解决这个问题的方法是要么永远不要使用 Synchronize 和 SendMessage(最好的方法),或者至少在任何锁之外使用它们。
在析构函数中或不在析构函数中释放 TCriticalSection 的位置并不重要。
但在释放 TCriticalSection 之前,您必须确保所有可能使用它的线程都已停止或处于无法再尝试进入此部分的状态。
例如,如果您的线程在分派网络消息时进入此部分,则必须确保网络已断开连接并且所有待处理消息均已处理。
如果不这样做,在大多数情况下会触发访问冲突,有时什么也不会发生(如果你幸运的话),并且很少会引发死锁。
Yes it is. But the problem is likely not in the destruction. You probably have a deadlock.
Deadlocks are when two threads wait on two exclusive resources, each wanting both of them and each owning only one:
The way to fight these is to order your locks. If some thread wants two of them, it has to enter them only in specific order:
This way deadlock will not occur.
Many things can trigger deadlock, not only TWO critical sections. For instance, you might have used SendMessage (synchronous message dispatch) or Delphi's Synchronize AND one critical section:
Synchronize and SendMessage send messages to Thread1. To dispatch those messages, Thread1 needs to finish whatever work it's doing. For instance, OnPaint handler.
But to finish painting, it needs FooLock, which is taken by Thread2 which waits for Thread1 to finish painting. Deadlock.
The way to solve this is either to never use Synchronize and SendMessage (the best way), or at least to use them outside of any locks.
It does not matter where you are freeing TCriticalSection, in a destructor or not.
But before freeing TCriticalSection, you must ensure that all the threads that could have used it, are stopped or are in a state where they cannot possibly try to enter this section anymore.
For example, if your thread enters this section while dispatching a network message, you have to ensure network is disconnected and all the pending messages are processed.
Failing to do that will in most cases trigger access violations, sometimes nothing (if you're lucky), and rarely deadlocks.
使用 TCriticalSection 以及临界区本身并没有什么神奇之处。尝试用普通的 API 调用替换 TCriticalSection 对象:
切换到 API 不会损害代码的清晰度,但也许有助于揭示隐藏的错误。
There are no magical in using TCriticalSection as well as in critical sections themselves. Try to replace TCriticalSection objects with plain API calls:
Switching to API will not harm clarity of your code, but, perhaps, help to reveal hidden bugs.
您需要使用 try..finally 块来保护所有关键部分。
使用 TRTLLCriticalSection 而不是 TCriticalSection 类。它是跨平台的,TCriticalSection 只是它的一个不必要的包装。
如果数据处理过程中出现任何异常,那么临界区就没有留下,另一个线程可能会阻塞。
如果您想要快速响应,您还可以将 TryEnterCriticalSection 用于某些用户界面过程等。
以下是一些好的实践规则:
这是一些代码示例:
You NEED to protect all critical sections using a try..finally block.
Use TRTLCriticalSection instead of a TCriticalSection class. It's cross-platform, and TCriticalSection is only an unnecessary wrapper around it.
If any exception occurs during the data process, then the critial section is not left, and another thread may block.
If you want fast response, you can also use TryEnterCriticalSection for some User Interface process or such.
Here are some good practice rules:
Here is some code sample:
如果应用程序中唯一的显式同步代码是通过关键部分,那么追踪它应该不会太困难。
您表明您只看到终止时的僵局。当然,这并不意味着它不会在您的应用程序正常运行期间发生,但我的猜测(我们必须在没有更多信息的情况下猜测)是这是一个重要的线索。
我假设该错误可能与强制终止线程的方式有关。如果一个线程在仍持有锁的情况下终止,但另一个线程在有机会终止之前尝试获取锁,则会发生您所描述的死锁。
可以立即解决问题的一个非常简单的方法是确保正如其他人正确所说的那样,锁的所有使用都受到 Try/Finally 的保护。这确实是一个关键点。
Delphi 中的资源生命周期管理有两种主要模式,如下所示:
另一种主要模式是在 Create/Destroy 中配对获取/释放,但这在锁的情况下不太常见。
假设您对锁的使用模式正如我所怀疑的那样(即在同一方法内获取和释放),您能否确认所有使用都受到 Try/Finally 的保护?
If the only explicit synchronisation code in your app is through critical sections then it shouldn't be too difficult to track this down.
You indicate that you have only seen the deadlock on termination. Of course this doesn't mean that it cannot happen during normal operation of your app, but my guess (and we have to guess without more information) is that it is an important clue.
I would hypothesise that the error may be related to the way in which threads are forcibly terminated. A deadlock such as you describe would happen if a thread terminated whilst still holding the lock, but then another thread attempted to acquire the lock before it had a chance to terminate.
A very simple thing to do which may fix the problem immediately is to ensure, as others have correctly said, that all uses of the lock are protected by Try/Finally. This really is a critical point to make.
There are two main patterns for resource lifetime management in Delphi, as follows:
The other main pattern is pairing acquisition/release in Create/Destroy, but that is far less common in the case of locks.
Assuming that your usage pattern for the locks is as I suspect (i.e. acquireand release inside the same method), can you confirm that all uses are protected by Try/Finally?
如果您的应用程序仅在退出时挂起/死锁,请检查所有线程的终止事件。如果主线程发出信号通知其他线程终止,然后在释放它们之前等待它们。重要的是不要在终止事件中进行任何同步调用。当主线程等待工作线程终止时,这可能会导致死锁。但同步调用正在主线程上等待。
If your application only hangs/ deadlocks on exit please check the onterminate event for all threads. If the main thread signals for the other threads to terminate and then waits for them before freeing them. It is important not to make any synchronised calls in the on terminate event. This can cause a dead lock as the main thread waits for the worker thread to terminate. But the synchronise call is waiting on the main thread.
不要在对象的析构函数中删除关键部分。有时会导致您的应用程序崩溃。
使用单独的方法删除关键部分。
过程 someobject.deleteCritical();
开始
删除关键部分(关键部分);
结尾;
析构函数 someobject.destroy();
开始
// 在这里执行您的发布任务
结尾;
1)你调用删除临界区
2)释放(free)对象后
Don't delete critical sections at object's destructor. Sometimes will cause your application to crash.
Use a seperate method which deletes the critical section.
procedure someobject.deleteCritical();
begin
DeleteCriticalSection(criticalSection);
end;
destructor someobject.destroy();
begin
// Do your release tasks here
end;
1) You call delete critical section
2) After you release(free) the object