死锁 Delphi 解释/解决方案

6
在一个服务器应用程序中,我们有以下内容: 一个名为JobManager的单例类。 另一个类Scheduler,它不断检查是否到了将任何类型的作业添加到JobManager的时间。
当该执行时,Scheduler会执行以下操作:
TJobManager.Singleton.NewJobItem(parameterlist goes here...);

同时,在客户端应用程序上,用户执行某些操作会触发对服务器的调用。在内部,服务器向自己发送一条消息,监听该消息的类之一是JobManager。 JobManager处理该消息,并知道现在是时候将新作业添加到列表中,调用自己的方法:

NewJobItem(parameter list...);

在NewJobItem方法中,我有类似以下的代码:
  CS.Acquire;
  try
    DoSomething;
    CallAMethodWithAnotherCriticalSessionInternally;
  finally
    CS.Release;
  end;

这里发生了一个死锁,发生在系统到达这个点(CS.Acquire)时。客户端和服务器应用程序之间的通信是通过Indy 10进行的。我认为,触发服务器应用程序方法并向JobManager发送消息的RPC调用正在Indy线程的上下文中运行。
调度程序有自己的线程运行,并且直接调用JobManager方法。这种情况容易导致死锁吗?有人能帮我理解为什么会出现死锁吗?
我们知道,有时候当客户端执行特定操作时,会导致系统锁定,然后我最终找到了这个关键点,在同一类的关键部分被从不同的点(调度程序和JobManager的消息处理方法)访问两次。
还有一些额外的信息需要补充,我想说的是(这可能很愚蠢,但无妨),DoSomething中还有另一个


  CS.Acquire;
  try
    Do other stuff...
  finally
    CS.Release; 
  end;

这个内部的CS.Release会对外部的CS.Acquire造成影响吗?如果是的话,这就可能是调度器进入临界区的时候,所有的锁定和解锁都变得混乱的地方。


临界区的目的是保护不能在不同线程上同时执行的代码,因此如果同一实例上达到了两次(或更多次)关键部分,则可以。这意味着关键部分正在那里执行它的工作,并且它是为此而设计的!不好的情况是当CS1不释放,因为其获取线程正在等待获取CS2,而CS2不释放,因为其获取线程正在等待获取CS1(称为死锁)。从您的话中,我不确定您是否遇到了死锁...你为什么确定? - jachguate
在调试时,我跟随消息处理程序方法,刚进入了关键部分,我的断点在CS.Acquire行被再次触发,来自调度程序 - 再次按F8,系统停止。 - ronaldosantana
5
你还可以在Windows消息处理循环中发生死锁。当ThreadA获得CS1的锁并执行需要消息循环的调用时,Thread B正在等待获取CS1的同时实际上停止、中断或无限期延迟消息循环... - Marjan Venema
1
调度程序等待是可以的...百万美元的问题在于先获得关键部分的线程正在做什么,为什么它没有释放它?如果它正在等待...那么问题是它在等待什么? - jachguate
我刚刚添加了一些信息 - 尝试理解在嵌套的cs.acquire;...cs.release环境中会发生什么... - ronaldosantana
1个回答

2
目前系统没有足够的信息来确定您的JobManager和Scheduler是否会导致死锁,但如果它们都调用相同的NewJobItem方法,那么这不应该是问题,因为它们将以相同的顺序获取锁。
至于您关于NewJobItem CS.acquire和DoSomething CS.acquire之间是否会产生互动的问题:这取决于情况。如果在两种方法中使用的锁对象不同,则两个调用应该是独立的。如果是相同的对象,则取决于锁的类型。如果您使用的是可重入锁(例如,它们允许从同一线程多次调用acquire并计算它们已被获取和释放的次数),则这不应该是问题。另一方面,如果您有简单的锁对象不支持重新进入,则DoSomething CS.release可能会释放该线程的锁,然后CallAMethodWithAnotherCriticalSessionInternally将在没有NewJobItem中获取的CS锁保护的情况下运行。
死锁发生在有两个或更多线程运行时,并且每个线程在继续自己之前都在等待另一个线程完成其当前工作。
例如:
Thread 1 executes:

lock_a.acquire()
lock_b.acquire()
lock_b.release()
lock_a.release()


Thread 2 executes:

lock_b.acquire()
lock_a.acquire()
lock_a.release()
lock_b.release()

注意,在线程2中获得的锁的顺序与线程1相反。现在,如果线程1获取lock_a,然后被中断,线程2现在运行并获取lock_b,然后开始等待lock_a可用才能继续。然后线程1继续运行,它要做的下一件事是尝试获取lock_b,但它已经被线程2占用,所以它等待。最终,我们处于线程1正在等待线程2释放lock_b而线程2正在等待线程1释放lock_a的情况中。
这就是死锁现象。
有几种通用解决方案:
  1. 在所有代码中只使用一个共享全局锁。这样就不可能让两个线程等待两个锁。这会使您的代码等待锁变得很慢。
  2. 始终只允许您的代码一次持有一个锁。这通常太难控制,因为您可能不知道或控制方法调用的行为。
  3. 只允许您的代码同时获取多个锁,并同时释放它们,并禁止在已经获取锁的情况下获取新的锁。
  4. 确保所有锁都按照相同的全局顺序获取。这是一种更常见的技术。
使用第四种解决方案时,您需要小心编程,并始终确保以相同的顺序获取锁/关键部分。为了帮助调试,您可以在系统中的所有锁上放置全局顺序(例如,每个锁只有一个唯一的整数),然后如果您尝试获取当前线程已经获取的锁的低级别锁(例如,如果new_lock.id < lock_already_acquired.id,那么抛出异常)则会引发错误。
如果您无法放入全局调试工具来查找已按顺序获取的锁,则建议您找到代码中获取任何锁的所有位置,并使用当前时间、调用acquire/release的方法、线程ID和正在获取的锁ID打印调试消息。也对所有释放调用执行相同的操作。然后运行系统,直到出现死锁,并在日志文件中查找哪些线程以及以哪种顺序获取了锁。然后决定哪个线程正在以错误的顺序访问其锁并进行更改。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接