多线程WIN32程序如何检查死锁(三)-使用Petri网络来表示多线程应用程序

根据1993版的《计算机百科全数》,Carl Adam Petri一个在德国波恩为Gesellschaft fuer Mathematik und Datenverarbeitung工作,我花了很长时间猜测为什么他的理论并没有引起当时学术和工商界的本来应得的注意。Petri网络早在19世纪70年代就已经深入的研究过,以我浅见,他们提供了一个分析和研究并发系统优秀的架构,对于Petri网络的最好的和更广泛的讨论在T.MurataPetri Nets: Properties, Analysis and Applications论文中阐述,IEEE77次(19894月)会议录的541-580。本节主要讲述Petri网络的最主要方面,在下一节我们可以看到怎么样在编写WIN32 API应用程序的时候使用他们。

Petri网络非常的通用,并不仅仅用来模拟多线程应用程序,例如:计算机硬件和工作过程也可以用Petri网络来描述。本文将限定Petri网络的讨论范围为多线程应用程序。

Petri网络是一个有向图,它的节点可以是位置,或转换,前者被画作一个圆,它粗略地描述一个线程的当前状态,后者被画作一个矩形,它粗略的描绘从一个状态到另一个状态变换的行为。边存在于位置和转换之间,也就是说两个状态或位置不能直接彼此互相连接,然而,两个到多个位置能够连接到同一个转换(对于模拟同步来说很重要),一个转换可以连接一到多个位置(模拟非确定的系统很重要)。

为了模拟一个应用程序的动态行为,个别的位置能够标住(在Petri网络中通常在圆圈中画一个黑点来表示)来表示线程当前执行代码处于对应的状态。有一个简单的激活原则:如果所有转换的导入位置(前驱或输入)被标记并且所有转换连接的位置(后继或输出)没有被标记,那么该转换被激活。如果一个转换能被激活,那么它可以删除所有前驱节点的位置标记并标记所有后继的位置。注意,通常架构的Petri网络具有相当的弹性:在自由的Petri网络中,每个位置都有描述一个位置同时至多可以描述多少个标记的能力,并且每个边都关联一个数字,该数字来表示当边被旋转的时候多少个标记被删除或插入到某个位置。本文我们仅仅讨论所谓的1-conservative Petri网络,这种Petri网络中每个位置的容量假定为1并且在边连接位置到转换的时候删除一个标记,连接转换到一个位置时增加一个标记。

在上面的优点枯燥的讨论之后,我们来看一个例子-怎么样将GOOFY转换成一个Petri网络。

1GOOFYPetri的网络图

这个网络仅仅描述了两个线程都能访问的while循环部分。网络由10个位置组成,每个线程由四个位置组成,相应对应的有四个状态:

1.  等待两个关键段(p11p21

2. 都声明了各自的关键段,但正在等待另一个(p12p22

3. 都声明了两个关键段(p13p23

4. 释放了第二个声明的关键段,而不是第一个(p14p24

剩下的两个位置表示关键段1和关键段2。相对于线程状态的位置指示了线程执行的代码段,而关键段对应的位置标记暗示该关键段是自由的。

就像图2所示,当状态”es 1”激活时,从p11cs2的标记被删除同时一个标记被插入p12

2,一个转换被激活后的GOOFY网络图

我们看到“ecs1”和“ecs2”转换仅仅当线程等待某关键段而该关键段是空闲的时候才被激活。“ecs1”表示EnterCriticalSection(&cs1)语句,“ecs2”表示语句EnterCriticalSection(&cs2),同样“lcs1”和“lcs2”分别表示语句LeaveCriticalSection(&cs1)和LeaveCriticalSection(&cs2)。网络上的标记说明了应用程序的初始标记,也就是说,两个线程都在各自while循环的顶部,并准备声明他们的第一个关键段,这些关键段是没有被任何线程拥有。注意我们对线程拥有一个关键段的时候确切做了什么并不感兴趣。在我们早先提到的GOOFY版本中,两个线程向屏幕输出,但位置或许表达任意个计算。例如,在一个WIN NT的多线程应用程序,它被用来通过一个关键段或mutex对象来向设备内容输出,但对于Petri网络来模拟它时,他在拥有关键段的时候所做的并不感兴趣。只要关键段中的代码没有请求任何其他的同步资源或没有做任何与其他线程交互的工作,我们仅仅将他们描述为一个位置。这是相当重要的。Petri网络是一个应用程序的抽象,也就是说,它隐藏了很多信息。理论上可能将应用程序的语句1比1的转换为Petri网络,也就是说每个程序中的单个语句都是一个状态并在描述两个后续语句的所有转换之间添加一个位置,但我们使用Petri网络仅仅来模拟线程之间的同步和交互,因此许多中间的位置和转换可以尽量的省略,因为这会使网络不容易描述重点,并且省略之后并没有改变程序并发的特性。在“Putting DLDETECT to Work”一文中我们将看到需要被考虑的最小的WIN32 API集合。Petri网络中其他的抽象就是定时行为了。例如:我们设置了一个刷新屏幕定时器是500ms。在Petri网络描述的应用程序中,定时器的间隔长度就丢失了,在这个理论中没有办法来具体表现它。两个定时器,一个是500ms的间隔,另一个是2s的间隔,在Petri网络中,他们看起来是相似的。尽管有人可能认为这是一个缺陷,但实际上不是。任何关于定时器间隔关系的假设对一个应用程序的正确性来说都是非常危险的,因为定时器的异步特性。所有的Petri模型都是这样的事实,异步事件,不是他们什么时候发生。

同样,在描述GOOFY的Petri网络中有一个不确定的度,也就是说,有一种情况就是多于一个的转换能激活。Petri网络理论没有规定该案例下的任何顺序,两个可激活的转换都可以激活并改变整个系统的状态,这精确地模拟了GOOFY的不确定性,也就是我们不能假定哪个线程将首先进入关键段。

现在我们手边有了转换多线程应用程序为Petri网络的工具了,你或许会问:“啊。。。?是的我们现有有了精密复杂的蜘蛛网来代替一行行的代码,但我们能得到什么?”好问题,Petri网络理论除了给我们代码可视化的多线程应用程序的控制流程,自身看似没有太多的优点,然而,它证明了作为坚实数学分析的基础,并告诉我们很多应用程序的信息。让我们将网络看作一个矩阵,行对应于位置,列对应于转换。让我们在矩阵中输入(x, y),当转换y激活的时候,删除从位置x删除一个标记,设置为-1,当在x处放置一个标志设置为1,否则为0。这个矩阵m叫做网络的关联矩阵,让我们来看一下GOOFY的关联矩阵。

Figure 3. Incidence matrix for GOOFY

上面显示了方程式V*m=0(零矩阵空间)的解V是枚举应用程序中所有不变量的向量空间基础。也就是说,所有上面方程的解都是应用程序的不变量,并且他们是线性独立的,所有解的线性合并也是不变量。

讨厌,这是线性代数!当我第一次阅读这片论文的时候,我希望我能够逃避掉矩阵、行列式、无穷数列以及所有等等,但我不能,因此现在我们也要介绍这些。并且由于线性合并、基数、0空间和所有这些术语都倾向于抽象,下面让我们看看怎么样在实际当中运用,也就是在我们臭名昭著的GOOFY中使用。使用一个分类、魔术色彩的算法,这些算法是严格地从德鲁依的嘴巴到德鲁依的耳朵(德鲁依是具有魔法能力的男子),仅仅开玩笑,这个算法在《The Implementation of DLDETEC.EXE》中描述,我计算GOOFY的关联矩阵的0空间的解,他们如下:

Invariant No.    |p11 p12 p13 p14 cs1 cs2 p21 p22 p23 p24

-----------------|---------------------------------------

     1           | 1   1   1   1   0   0   0   0   0   0

     2           | 0   0  -1   0  -1   0   1   0   0   0

     3           | -1  0   0   0   0   1   0   0   1   0

     4           | 1   0   1   0   1  -1   0   1   0   1

这些数据需要精心的研究。十列表示关联矩阵中出现的10个位置,我是根据关联矩阵来标注的。四个向量表示一个不变量,也就是说根据每个不变向量,网络中的所有标记都是常量,省略讨厌的0,我们可以写成下面的形式:

p11+p12+p13+p14 = 常量
p21-p13-cs1 =
常量
cs2+p23-p11 =
常量
p11+p13+cs1-cs2+p22+p24 =
常量

注意,这些不变量并没有告诉我们应用程序的运行时行为,并且他们并不依赖于任何特殊的初始标记。它们能够告诉我们的是:一旦我们选择了任何初始的标记,对所有方程来说,任何曾经能通过任何转换激活的有效顺序的标记得到的状态将产生和初始产生相同的结果。让我们用例子来解释:当p11,p21,cs1,cs2被标记的时候得到GOOFY的初始标记。上面方程的初始标记产生如下的结果:

1*p11+0*p12+0*p13+0*p14=1
1*p21-0*p13-1*cs1=0
1*cs2+0*p23-1*p11=0
1*p11+0*p13+1*cs1-1*cs2+0*p22+0*p24=1

没有任何初始标记延伸的标记能干扰不变量。例如:假如两个线程都被运行同时执行各自的代码段,而都声明关键段,他们相当于一个标记p13p23,这个时候程序是不正确的。插入这些标记到不变量方程得到下面的结果:

0*p11+0*p12+1*p13+0*p14 = 1
0*p21-1*p13-0*cs1 = -1
0*cs2+1*p23-0*p11 = 1
0*p11+1*p13+0*cs1-0*cs2+0*p22+0*p24 = 1

第二和三个结果不同于初始标记的结果,因此<p13,p23>标记是不可达的。另一方面,死锁标记<p12,p22>产生下面的结果:

p11+p12+p13+p14 = 1
p21-p13-cs1 = 0
cs2+p23-p11 = 0
p11+p13+cs1-cs2+p22+p24 = 1

因为和初始标记的结果相同,死锁标记是可达的。

不幸的是,对于判断标记是否可达,符合不变量仅仅是一个必要而非充分条件。例如:标记<p14,p24>满足所有的不变量但是不可达的。对于某种类型的Petri网络,必要的和充分的条件能被公式化,但这些网络类型一般太严格而在模拟多线程分析的时候不是很有用。

在一个普通的Petri网络中,是否任何给定的标记都能从另一个标记得到的问题被证明为至少是指数级复杂的,对于一个很大的网络来说实在太昂贵的代价了,因此这样的情况下,DEADLOCOP能多就是一个警告:“恩,这里有一个死锁标记也许能或不能从初始标记得到,因此你应该手动的检查,祝你”(用户双击结束DEADLOCOP)。那么对于一个没有死锁标记符合不变量方程的时候,DEADLOCOP可能说:“祝贺你,我没有在网络中找到死锁”(这个消息被愉快的用户重复)。

注意,为了是一个标记成为从另一个标记可达的候选者,标记对任何方程集合产生相同的结果是不够的,也就是所有的方程必须满足不变量条件。例如:假如我们仅仅看第一和四个方程,不可能达标记<p13,p23>将是潜在的可达的,但第二和第三个方程告诉我们不是这样的。

让我们再来看看GOOFY例子,按照前面的规定,不变量分析生命那些能产生初始状态可达的所有状态结果的不变量方程。如果我们修改GOOFY使主线程不再产生线程2,网络看起来是相同的,但初始标记将从<p11, cs1, cs2, p21> 变为 <p11, cs1, cs2>;换句话说,网络的右边的线程2,将不在有标记。现在不变量方程如下:

1*p11+0*p12+0*p13+0*p14 = 1
0*p21-0*p13-1*cs1 = -1
1*cs2+0*p23-1*p11 = 0
1*p11+0*p13+1*cs1-1*cs2+0*p22+0*p24 = 1

现在,死锁标记<p12,p22>是不可达的了,因为他违反了第二个不变量,因此没有了第二个线程运行,系统中也就没有了死锁。仅仅一个线程运行,不可能产生死锁,因为死锁的条件是要求至少两个线程一个循环等待!我们已经正式的证明了每个人都感觉上知道的声明了。

现在,死锁标记 <p12,p22> 是不可达的了,因为他违反了第二个不变量,因此没有了第二个线程运行,系统中也就没有了死锁。仅仅一个线程运行,不可能产生死锁,因为死锁的条件是要求至少两个线程一个循环等待!我们已经正式的证明了每个人都感觉上知道的声明了。

你可能感兴趣的:(多线程WIN32程序如何检查死锁(三)-使用Petri网络来表示多线程应用程序)