目录
1. 学前回顾
我们在上一部分的学习中,介绍了正式建模语言的重要性,并将一个流程抽象地建模为一个状态机,它通过执行原子动作来进行,原子动作会改变其状态。进程的执行产生了一个原子动作的序列(trace)。我们现在研究如何对由多个进程组成的系统进行建模。
要考虑的第一个问题是如何模拟一个进程相对于另一个进程的执行速度。
我们选择不对相对速度进行建模,而只是说明进程以任意的相对速度执行。这意味着一个进程从一个动作进行到下一个动作可能需要任意长的时间。我们对执行时间进行了抽象。这样做的缺点是我们对程序的实时性一无所知,但其优点是我们可以独立于计算机及其操作系统的特定配置来验证其他属性。这种独立性对于并发程序的可移植性显然很重要。
下一个问题是如何对并发性或并行性进行建模。
除了通过交错执行来模拟并发的情况外,是否有必要对不同进程的动作可以由不同的处理器同时执行的情况进行建模?我们总是选择用交错执行来模拟并发性。如果一个模型允许动作以a→b的顺序或b→a的顺序发生,则一个动作a与另一个动作b是并发的。由于我们在模型中不表示时间,事件a与事件b实际发生在同一时间的事实并不影响我们可以断言关于并发执行的属性。
最后,在决定了并发执行的交错模型后,我们可以对代表并发程序执行的交错动作轨迹中不同进程的动作的相对顺序说些什么?
我们知道,来自同一进程的动作是按顺序执行的。然而,由于进程以任意的相对速度进行,来自不同进程的动作是任意交错的。任意交错原来是并发执行的一个很好的模型,因为它抽象了处理器在进程之间因外部中断而切换的方式。相对于进程的执行,中断的时间一般不能被预先确定,因为现实世界中的行动不能被准确预测——我们不能预知未来。
并发执行模型中,进程以任意的相对速度按任意的顺序执行动作,被称为异步执行模型。它与同步模型形成鲜明对比,在同步模型中,进程以同步的执行步骤执行动作,有时被称为锁步。
2. FSP—Parallel composition
并行组合操作符允许我们描述两个进程的并发执行。
(P || Q)
如果P和Q是进程,那么 ( P || Q ) 表示P和Q的并发执行。操作符 || 是并行组合操作符。
并行组合产生一个进程,它与其他进程一样被表示为一个状态机。代表该组合的状态机产生其组成进程的所有可能的交织traces。例如,进程:
ITCH = (scratch->STOP).
有一个由动作scratch组成的单一trace(一个原子动作的序列)。这个进程:
CONVERSE = (think->talk->STOP).
有单一的tarce: think→talk。复合进程:
||CONVERSE_ITCH = (ITCH || CONVERSE).
有以下trace:
think → talk → scratch
think → scratch → talk
scratch → think → talk
下图描述了与ITCH、CONVERSE和CONVERSE_ITCH对应的状态机。
代表构成的状态机是由其组成成分的笛卡尔积形成的。例如,如果ITCH处于状态(i),CONVERSE处于状态(j),那么这个组合状态就由CONVERSE_ITCH在状态(<i, j>)中表示。因此,如果CONVERSE执行了思考动作并处于状态(1),而ITCH执行了抓取动作并处于状态(1),那么在组合中代表这个的状态是状态(<1,1>)。这被描述为组合的状态(4)。
从上图还可以看出,划痕动作与思考和交谈同时发生,因为该模型允许这些动作以任何顺序发生,同时保留了思考必须发生在交谈之前的约束。换句话说,一个人必须在说话之前思考,但他可以在任何时候抓挠!这就是复合过程的定义。
复合过程的定义总是以||为前导,以区别于原始过程的定义。在之前的学习中讲过,原始过程是用动作前缀和选择来定义的,而复合过程只用平行组合来定义。为了确保 FSP 所描述的模型是有限的,需要保持原始过程和复合过程之间的这种严格区别。
作为进一步的例子,下面的流程为一个时钟收音机建模,它包含了两个独立的活动:一个滴答作响的时钟和一个可以打开和关闭的收音机。下图中描述了该组合的状态机。
CLOCK = (tick->CLOCK).
RADIO = (on->off->RADIO).
||CLOCK_RADIO = (CLOCK || RADIO).
下面给出了由上图的状态机产生的轨迹的例子。LTSA Animator可以用来生成这种轨迹。
on → tick → tick → off → tick → tick → tick → on → off → ...
tick → on → off → on → off → on → off → tick → on → tick → ...
并行组合算子服从于一些简单的代数法则:
Commutative: (P||Q) = (Q||P)
Associative: (P||(Q||R)) = ((P||Q)||R) = (P||Q||R).
这也就说明了括号和排列顺序是无关紧要的。
上面的例子是两个简单的进程(单一进程)在交错,但是也可以将复合的进程们当作 first-class citizens 去和别的单一进程或者复合进程们进行交错。
例如我们将 ITCH | | CONVERSE 和 一个新进程 LAUGH 进行交错:
ITCH = (scratch -> STOP).
CONVERSE = (think -> talk-> STOP).
LAUGH = (laugh -> LAUGH).
||CONVERSE_ITCH = (ITCH || CONVERSE).
||CONVERSE_ITCH_LAUGH = (CONVERSE_ITCH || LAUGH).
3. FSP—Shared actions
3.1 “共享行为”概念
之前的例子都是具有不相干字母的进程的组合。也就是说,一个组合中的进程没有任何共同的动作。如果一个组合中的进程有共同的动作,那么这些动作被称为共享的。共享动作是流程交互被建模的方式。虽然非共享动作可以任意交错,但共享动作必须由参与该共享动作的所有进程同时执行。下面的例子是共享动作的进程的组成,即满足。
BILL = (play -> meet -> STOP).
BEN = (work -> meet -> STOP).
||BILL_BEN = (BILL || BEN).
构成的可能执行traces是:
play → work → meet
work → play → meet
未共享的行动,即play和work,是并发的,因此可以按任何顺序执行。然而,这两个动作被限制在共享动作相遇之前发生。共享动作使进程BILL和BEN的执行同步。下图描述了复合过程的状态机。
下一个例子包括一个制造物品的流程,然后通过一个共享的就绪动作发出信号,表明该物品已经可以使用。用户只有在就绪动作发生后才能使用该物品。
MAKER = (make->ready->MAKER).
USER = (ready->use->USER).
||MAKER_USER = (MAKER || USER).
make → ready → use → make → ready → ...
make → ready → make → use → ready → ...
在最初的物品被制造出来并准备好之后,制造和使用可以平行进行,因为制造和使用的动作可以按任何顺序发生。然而,一个物品在被使用之前总是被制造出来的,因为在所有traces中第一个动作都是制造。第二个跟踪显示,在第一个项目被使用之前,可以有两个项目被制造。假设这是不可取的行为,我们不希望MAKER过程以这种方式获得进展。解决办法是修改模型,使用户表示项目被使用。这个使用的动作与制造者共享,制造者现在不能继续制造另一个项目,直到第一个项目被使用。这个第二个版本显示在下面和图中。
MAKERv2 = (make->ready->used->MAKERv2).
USERv2 = (ready->use->used->USERv2).
||MAKER_USERv2 = (MAKERv2 || USERv2).
第二个版本中MAKER和USER之间的交互是一个握手的例子,一个被另一个承认的动作。握手协议被广泛用于构造进程间的交互。请注意,我们的交互模型并不区分哪个进程唆使了一个共享动作,尽管我们很自然地认为制造者进程唆使了准备好的动作,而用户进程唆使了使用的动作。然而,如前所述,由一个流程唆使的输出动作通常不构成选择的一部分,而输入动作则可能。
到目前为止,同步的例子是在两个进程之间;但是,许多进程可以参与一个共享动作。下一个例子说明了多方同步在一个小型制造系统中的应用,该系统生产两个不同的零件并将零件组装成一个产品。在两个零件都准备好之前,不能进行组装。同样,制造者不允许领先于用户。下图描述了该状态机。
MAKE_A = (makeA->ready->used->MAKE_A).
MAKE_B = (makeB->ready->used->MAKE_B).
ASSEMBLE = (ready->assemble->used->ASSEMBLE).
||FACTORY = (MAKE_A || MAKE_B || ASSEMBLE).
由于进程的平行组合本身就是一个进程,被称为复合进程,所以它可以被用于定义进一步的组合。我们可以通过从MAKE_A和MAKE_B创建一个复合过程来重组前面的例子,如下所示。
MAKE_A = (makeA->ready->used->MAKE_A).
MAKE_B = (makeB->ready->used->MAKE_B).
||MAKERS = (MAKE_A || MAKE_B).
ASSEMBLE = (ready->assemble->used->ASSEMBLE).
||FACTORY = (MAKERS || ASSEMBLE).
3.2 案例学习
我们上一篇文章使用了红绿灯系统模型作为例子,这里我们将继续使用并扩展它。
TRAFFIC_LIGHT = (button -> YELLOW | idle -> GREEN),
GREEN = (green -> TRAFFIC_LIGHT),
YELLOW = (yellow -> RED),
RED = (red -> TRAFFIC_LIGHT).
PEDESTRIAN = (wander -> PEDESTRIAN).
||LIGHT_PEDESTRIAN = (TRAFFIC_LIGHT || PEDESTRIAN).
思考上面的模型,红绿灯和行人没有形成真正的交互。虽然红绿灯一直遵循着规则,但行人并没有遵循红绿灯的规则,而只是在漫无目的的闲逛。
那我们如何实现行人和红绿灯的真正交互呢,首先交互是通过共享的动作进行的:
如果一个组合中的进程有共同的行动,这些行动就被称为共享的。这就是进程交互的建模方式。虽然不共享的动作可以任意交错,但共享的动作必须由参与该共享动作的所有进程在同一时间执行。
3.3 Shared actions
在下面的例子中,共享行为是 “button”。这个模型模拟了行人按下交通灯按钮的动作,所以按按钮的行为是被红绿灯进程和行人进程共享的:
TRAFFIC_LIGHT = (button -> YELLOW | idle -> GREEN),
GREEN = (green -> TRAFFIC_LIGHT),
YELLOW = (yellow -> RED),
RED = (red -> TRAFFIC_LIGHT).
PEDESTRIAN = ( button -> PEDESTRIAN
| wander -> PEDESTRIAN
).
||LIGHT_PEDESTRIAN = (TRAFFIC_LIGHT || PEDESTRIAN).
我们从这个模型中,并不能看出是哪个进程正在执行按按钮的操作,因为按按钮行为是共享的:两个进程都参与其中。当然,我们会有意识地去认为按按钮行为由行人输出,然后输入到红绿灯。但我这里想强调的是,FSP并不展现这一点,共享行为就仅代表行为被进程共享。
4. FSP—进程标签
考虑到一个进程的定义,我们经常想在程序或系统模型中使用该进程的一个以上的副本。例如,给定一个开关的定义。
SWITCH = (on->off->SWITCH).
我们可能希望描述一个由两个不同开关组成的系统。然而,如果我们把这个系统描述为(SWITCH || SWITCH),那么这个组合就与单个开关没有区别,因为这两个开关进程在它们的共享动作上是同步开启和关闭。我们必须确保每个SWITCH进程的动作不是共享的,也就是说,它们必须有不相干的标签。为了做到这一点,我们使用了进程的标签构造。
a:P将P的字母表中的每个动作标签前缀为 "a"。
一个有两个开关的系统现在可以被定义为。
||TWO_SWITCH = (a:SWITCH || b:SWITCH).
下图给出了进程 a:SWITCH 和 b:SWITCH 的状态机表示。很明显,这两个进程的字母表是不相连的,即 {a.on, a.off} 和 {b.on, b.off} 。
使用一个参数化的复合过程SWITCHES,我们可以在FSP中描述一个开关阵列,如下所示:
||SWITCHES(N=3) =(forall[i:1..N] s[i]:SWITCH).
一个等同但更简短的定义是:
||SWITCHES(N=3) =(s[i:1..N]:SWITCH).
进程也可以用一组前缀标签来标记。这种前缀的一般形式如下:
{a1,...,ax}::P用标签a1.n,...,ax.n替换P的字母表中的每个动作标签n。此外,P定义中的每个过渡(n->Q)被替换为过渡({a1.n,...,ax.n}->Q)。
我们在下面的例子中解释这一设施的使用。对一个资源的控制是由以下进程来模拟的:
RESOURCE = (acquire->release->RESOURCE).
资源的用户是由进程来模拟的:
USER = (acquire->use->release->USER).
我们希望对一个由两个用户组成的系统进行建模,这两个用户共享资源,在同一时间只有一个用户可以使用它(称为 "相互排斥")。这两个用户可以使用进程标记来建模,即a:USER和b:USER。这意味着有两个不同的动作(a.acquisition和b.acquisition)来获取资源,同样也有两个动作来释放资源(a.release和b.release)。因此,RESOURCE必须被标记为{a,b}集合以产生这些转换。下文描述了这种构成。
||RESOURCE_SHARE =
(a:USER || b:USER || {a,b}::RESOURCE).
下图描述了RESOURCE_SHARE模型中流程的状态机表示。可以清楚地看到流程标签对RESOURCE的影响。复合流程图显示,一次只允许一个用户使用资源的预期结果已经实现。
我们的 "资源 "模型只允许一个用户获取资源,另一个用户释放资源。例如,它将允许以下的trace:
a.acquire → b.release → ...
然而,每个 USER 流程在成功地执行获取动作之前不能释放资源。因此,当 RESOURCE 与 USER 流程组合时,这种组合确保只有获取资源的同一个用户可以释放它。
我们尝试用这种方法去表示N个客户端加一个服务器的系统模型:
||N_CLIENT_SERVER(N=2)
= (forall[i:1..N] c[i]:CLIENT )
||
{c[i:1..N]}::(SERVER/{call/request , wait/reply})
).
5. FSP—Action relabelling
重标函数被应用于进程,以改变行动标签的名称。重标函数的一般形式是:
/{newlabel_1/oldlabel_1,...newlabel_n/oldlabel_n}.
重新标记通常是为了确保复合流程在所需的动作上同步。重标函数可以应用于原始流程和复合流程。然而,它一般在组合中使用得更频繁。
下面将描述一个提供某种服务的服务器进程和一个调用该服务的客户进程。
CLIENT = (call->wait->continue->CLIENT).
SERVER = (request->service->reply->SERVER).
正如所描述的那样,客户和服务器有不相干的字母,并且不以任何方式互动。然而,使用重新标记,我们可以将客户机的呼叫动作与服务器的请求动作联系起来,同样,回复和等待动作也是如此。该组合定义如下(用行为主体的行为代替接受方的行为)。
||CLIENT_SERVER = (CLIENT || SERVER)
/{call/request, reply/wait}.
在下图的状态机表示中可以看到应用重新标记功能的效果。在SERVER的描述中,标签call取代了request,在CLIENT的描述中,标签reply取代了wait。
5.1 通过前缀的重新标记
下面将使用限定或前缀的标签来描述客户—服务器系统的另一种表述。
SERVERv2 = (accept.request
->service->accept.reply->SERVERv2).
CLIENTv2 = (call.request
->call.reply->continue->CLIENTv2).
||CLIENT_SERVERv2 = (CLIENTv2 || SERVERv2)
/{call/accept}.
重标函数 /{call/accept} 将任何以accept为前缀的标签替换为以call为前缀的相同标签。因此,在复合进程CLIENT_SERVERv2中,accept.request成为call.request,accept.reply成为call.reply。当一个进程有一个以上的接口时,这种通过前缀的重新标记是很有用的。每个接口由一组动作组成,并且可以通过有一个共同的前缀来进行关联。如果需要进行组合,可以使用这个前缀对接口进行重新标记,就像在客户—服务器的例子中一样。
5.2 案例学习
在上面行人过马路的例子中,行人有两个行为,闲逛和按按钮,但其实,我们可以认为行人在没有按按钮时就是在闲逛。所以上面模型中属于行人的行为 wander 和属于红绿灯的行为 idle 可以被整合成同一个行为。
我们引入一个新语法:
P/{new1/old1, ..., newN/oldN}
给定一个进程P,上面表达式代表的进程和进程P是一样的,只是动作 old1 重命名为 new1 ,以此类推。
通过这个语法,我们就可以将我们可以将行人中的行为 wander 重新标记为行为 idle,如下所示:
TRAFFIC_LIGHT = (button -> YELLOW | idle -> GREEN),
GREEN = (green -> TRAFFIC_LIGHT),
YELLOW = (yellow -> RED),
RED = (red -> TRAFFIC_LIGHT).
PEDESTRIAN = ( button -> PEDESTRIAN
| wander -> PEDESTRIAN
).
||LIGHT_PEDESTRIAN = (TRAFFIC_LIGHT || PEDESTRIAN/{idle/wander}).
上面的模型是发生行为重新标记后的,我贴心的拿来了之前的模型方便你们对比。
6. FSP—Variable hiding
当应用于一个进程P时,隐藏操作符{a1...ax}从P的字母表中删除动作名称a1...ax,并使这些被隐藏的动作变得 "沉默"。这些沉默的动作被标记为tau。不同进程中的沉默行动是不共享的。
隐藏的动作变得不可察觉,因为它们不能与另一个进程共享,所以不能影响另一个进程的执行。隐藏对于降低大型系统的复杂性以达到分析目的是非常重要的,因为正如我们在后面所看到的,有可能将状态机的大小降到最低,以消除反应。隐藏可以应用于原始过程和复合过程,但一般用于定义复合过程。有时,说明可见的动作标签集并隐藏所有其他标签会更方便。
当应用于一个进程P时,界面操作符@{a1...ax}隐藏了P的字母表中所有不在a1...ax集合中标记的动作。
具体来说就是,
如果我们想降低复杂度,有两种办法,第一种是隐藏变量:
P\{a1,..,aN} is the same as P
但是名为 a1,..,aN 的这些行为被移除了。
Silent 行为被称为 tau,并且这些行为永远不被共享。
另一种是列出不被隐藏的变量:
P@{a1,..,aN} is the same as P
但是名为 a1,..,aN 的这些行为被移除了。
来看一个例子:
SERVER_1 = (request -> service -> reply -> SERVER_1)@{request , reply}.
SERVER_2 = (request -> service -> reply -> SERVER_2)\{service}.
这两个表达式是完全等价的。
再看一个例子:
下面的定义导致了图中描述的状态机。
USER = (acquire->use->release->USER)
\{use}.
USER = (acquire->use->release->USER)
@{acquire,release}.
USER的最小化消除了隐藏的 tau 动作,产生了一个具有同等可观察行为的状态机,但状态和转换较少。LTSA可以用来确认这一点。
以上就是使用FSP对并行的Processes建模的全部内容。