一种适应的正式框架
摘要
上下文包含环境中影响系统结构和行为的所有因素。自适应系统旨在根据上下文变化评估并调整自身。在基于组件的软件工程中,开发自适应系统是一个具有挑战性的问题。本文提出了一种基于进程代数的组件式适应的形式化模型与框架。基于该语义模型,我们提出了一种适应方法,该方法包括施加约束以限制组件的行为,以及使用变换算子来修改系统的架构。我们还将该方法集成到控制循环的使用中。通过医疗领域的简单应用对模型进行了仿真。
关键词 :自适应,Component,Semantic模型,Control循环
1 引言
近年来,研究界对自适应系统(self-adaptation system)的兴趣急剧增长,并已投入大量努力,以探索阐明自适应理论与实践中的挑战和原则的新方法。
上下文感知系统作为一类自适应系统,能够响应上下文变化并做出反应和调整。该进程从获取上下文信息开始,到检查相关变化并实施适应为止,在文献中被称为控制循环模型,通常与控制理论、自主计算或人工智能相关。
反馈环是自适应的通用机制。IBM提出了MAPE-K参考模型[1],这是首个明确揭示反馈环的自适应系统架构。MAPE-K由监控器、分析器、规划器和执行器组成,它们共享一个知识库。
软件组件适应是基于组件的软件工程中一个被广泛认可的问题。现有工作主要集中在组件行为(即程序行为的变化)和/或结构(即组件组合的变化)的适应上。在大多数情况下,这些系统通过非正式的图形表示法来描述,这些图用于说明组件之间的互连,或通过描述系统行为而忽略其结构的模型来表示。
本文中,我们提出了一种受米尔纳的进程代数[2]启发的基于组件的适应模型。该模型通过组件的行为来描述系统行为。这种组合通过操作符表达了一种架构。
基于该模型,我们通过施加约束和变换互连操作符来实现结构自适应。同时,我们将该方法集成到MAPE-K控制模型中。我们使用Prolog[3]对模型及该控制环路进行仿真。
本文其余部分组织如下:第2节总结了相关工作。第3节提出了一个用于自适应的语义模型。第4节详细说明了约束的施加。第5节解释了标记迁移系统。第6节介绍了操作符的替换。第7节详细描述了模型的实现。在第8节中,我们讨论了MAPE-K控制循环的使用。最后,第9节对全文进行总结,并展望了未来的工作。
2 相关工作
Arbab[4]提出了一种集成的基于通道的协调语言(Reo),该语言结合了结构和行为特性,应用于组件和服务组合领域。他们利用代数图变换实现分布式连接器的重新配置。Canal等人[5]提出了一个框架,统一了组件的行为适应与结构重配置,用于静态检测系统是否可以进行重新配置。他们还提出并形式化了不同类型的重配置及其属性。适配器被用来通过动作的相同名称来同步组件。巴斯蒂德等人[6]提出了一种旨在动态调整组件结构的同时保持其行为和服务的方法。他们的方法包括通过生成可适应的组件结构,并通过重构其内部结构来实现组件的适应。卡斯塔涅达和田村[7]提出了一种基于组件的架构,用于实现自适应所需的基本元素,包括监控、分析、规划和执行,且独立于被适应的系统。该架构提供了关注点分离和可扩展的实现能力。
3 用于适应的模型
3.1 架构描述
组件被描述为一个包含内部行为以及两组端口的实体:输入端口
inP
和输出端口
outP
。系统组件通过其输入和输出端口集合相互连接。每个端口由一个端口标签和通过该端口的数据类型(t)来描述。组件上的动作
k
对应于在其端口上发生的交换活动。它由一个端口标签
p
以及伴随的通过该端口传输的变量或值
(v)
。如果
type(v) = t
且
Cond
为真,则对两个动作
k1
和
k2
进行同步表示为
sync(k1, k2)
。
Cond
是应用于值的条件。
3.2 组件行为的语义
为了表达组件执行动作
k
并变换为
C′
的事实,我们使用以下表示法:
C!k C0
。换句话说,如果组件
C
的上下文确保动作
k
发生,则该组件变换为
C′
。组件之间可以通过多种方式互连,以表示各种不同的配置,如下所述。
调用 (Call()) :描述其输入和输出端口的激活。这从输入端口上的操作开始,到输出端口上的操作结束:
Call(C(inP; outP))!k C(inP\k, outP) if k ∈ inP (1)
Call(C(; outP))!k C(; outP\k) if k ∈ outP (2)
选择 (+) :只能调用其中一个组件:
C1 + C2!k C1′ if C1!k C1′ (3)
C1 + C2!k C2′ if C2!k C2′ (4)
并行 (||) :组件可以并行调用,并且可能同步。其中一个组件可以执行一个动作。动作可能会发生同步,在这种情况下,每个组件都推进到下一个状态:
C1 || C2!k C1′ || C2 if C1!k C1′ (5)
C1 || C2!k C1 || C2′ if C2!k C2′ (6)
C1 || C2!k C1′ || C2′ if C1!k1 C1′ and C2!k2 C2′ and sync(k1, k2) (7)
一个该操作符的版本,记为
|||
,仅使用前两条规则。
管道 (*) :如果一个组件的输出端口之一已经到达调用,则该组件可以开始在其某个输入端口上调用。
C1 * C2!k C1′ * C2 if C1!k C1′ and k ∈ inP(C1) (8)
C1 * C2!k C2′ if C1!k C1′ and C2!k C2′ and k ∈ outP(C1) ∩ inP(C2) (9)
以
P1({a,b},{c,d}) * P2({c,d},{g})
为例。{a,b} 和 {c,d} 分别是 P1 和 P2 的输入端口。{c,d} 和 {g} 是它们的输出端口。管道组合的结果为:
P1({a,b},{c,d}) * P2({c,d},{g})
!a P1({b},{c,d}) * P2({c,d},{g})
!b P1({}, {c,d}) * P2({c,d},{g})
4 约束
约束是一种组件,其作用是限制其他组件的行为。该构造将允许我们对系统施加约束,以使其适应当前上下文中的当前情况。我们使用一个记为
<
的操作符,其定义如下:
C1 < C2!k C1′ < C2′ if C1!k C1′ and C2!k C2′ (10)
如上所述,任何表达式均可用于表示约束。然而,我们可能需要表达通过动作序列构建的约束,该动作序列表示为
k; P!k P
,其中
P
为任意表达式或 null 组件。这使我们能够表达如下约束:
a; (b; null + c; d; call(p, {e,f}, {g}))
。
5 转换系统
系统行为可以表示为一个图,该图代表一个定义为元组的转换系统 ST:ST = (N, K, s0, tr)。其中,N 是节点集合,K 是动作集合,s0 是根节点(初始状态),tr 是转换函数,其表达形式如下:
tr: N × K → N such that tr(n1, k) = n2 if expr(n1)!k expr(n2) (11)
给定一个节点
n1
,我们定义
expr(n1)
为描述其行为的表达式。我们通过使用第 3 节中表述的规则从
n1
构建一个转换。例如,对于三个节点
n1
,
n2
和
n3
及其表达式:
-
expr(n1) = call(P1({g}), {}) || call(P2({a,b}), {b,d}) -
expr(n2) = call(P1({}, {}), {}) || call(P2({a}, {b,d}) -
expr(n3) = call(P1({g}), {}) || call(P2({}, {d})
我们有
tr(n1, a) = n2
和
tr(n1, b) = n3
。
从给定节点
n0
出发,我们可以执行一个动作序列
s
并到达节点
nm
。如果
s = k1, k2, ..., km
,我们定义从节点
n0
的这种执行如下:
n0!{s} nm
,如果存在中间节点
n1, n2, ..., nm-1
,使得
tr(ni, ki) = ni+1
。
6 操作符替换
给定一个表达式
P
,我们定义操作符替换
[[/]]
如下:
P [[op1/op2]]
是通过在
P
中将所有出现的操作符
op1
替换为
op2
而得到的。该变换的语义由以下规则给出:
(C1 op1 C2)[[op1/op2]] = C1[[op1/op2]] op2 C2[[op1/op2]] (12)
Call(inP, outP)[[op1/op2]] = Call(inP, outP) (13)
我们可以指定在层级
n
开始一个变换,如下所示:
(C1 op1 C2)[[op1/op2]]^n = C1[[op1/op2]]^(n−1) op2 C2[[op1/op2]]^(n−1); n > 1 (14)
(C1 op1 C2)[[op1/op2]]^1 = C1 op2 C2 (15)
我们还可以指定在层级
n
完成一个变换,如下所示:
(C1 op1 C2)[[op1/op2]]_n = C1[[op1/op2]]_(n−1) op1 C2[[op1/op2]]_(n−1); n > 1 (16)
(C1 op1 C2)[[op1/op2]]_1 = (C1 op1 C2)[[op1/op2]] (17)
如果
n = 0
,则每次出现的
op1
都将被替换为
op2
。通过这些变换,我们可以动态地修改架构和系统行为。例如,
-
(P1 || P2)[[||/+]]^1 = P1 + P2:将并行组合变换为选择。 -
((P1 || P2) || P3)[[||/+]]^2 = (P1 || P2) + P3:将并行中的第二个组件变换为选择。 -
(P1 || (P2 || P3))[[||/+]][[+/]] = P1 + P2 + P3:将并行组合变换为选择。
给定一个动作序列
s = k1, k2, ..., km
,其中
<n0, s, [op1/op2]^q>
,我们在执行
s
后应用一个变换。
q
表示开始变换的深度级别(下标)或希望完成变换的深度级别(上标)。
<n0; s, [op1/op2]^q> = n0!{k1,k2,...,km} nm+1[[op1/op2]]^q (18)
<n0, s, [op1/op2]_q> = n0!{k1,k2,...,k_m} nm+1[[op1/op2]]_q (19)
给定一个系统
P
及其约束
C
,如果
C
执行一个序列
s = a1,...,an
,则我们必须在
P
上执行相同的序列,即如果
C!{a1,...,an} C′
;那么存在
P′
,使得
P!{a1,a2...,an} P′
。因此,
P < C!{a1,a2...,an} P′ < C′
。
7 模型的实现
7.1 操作符的实现
我们在 Prolog 中实现了第 3 节中提出的推理规则,使用
infer/3
子句,该子句包含三个参数:当前表达式、要执行的动作以及执行此动作后得到的结果表达式。操作符采用固定的后缀表示法,即
op(X1, X2)
。我们使用
inPorts/2
和
outPorts/2
子句来确定组件的输入和输出端口。通过这些表示法,我们在表 1 中给出了实现这些操作符的规则。
调用操作符需要特殊处理,因为它使用了在 Prolog 中不存在的通用操作符。为了实现它,我们使用了一种扩展机制。我们通过定义更简单的推断规则来处理,从而对类型为
call(P, InP, OutP)
的表达式进行变换。我们定义此扩展如下:
call(P, InP, OutP) = Σx∈InP x.call(P, InP\n{x}, OutP) (20)
call(P, [], OutP) = Σy∈OutP y.call(P, [], OutP\n{y}) (21)
我们未定义任何关于类型为
call(P, [], [])
的表达式的规则,该表达式提供终止元素。我们使用子句
bagof(C, P, X)
来实现扩展规则,该子句生成所有满足谓词
P
的元素
X
,结果存储在
C
中。
7.2 执行动作
为了使用上述语义规则执行动作,我们定义了用于组件行为执行的子句。它们还给出了执行运行的结果,如下面的示例所示。
表 1. Prolog 中的操作符
| 操作符 | 实现 |
|---|---|
| 顺序 |
infer(seq(A, P1), A, P1)
|
| 选择 |
infer(alt(P1, P2), A, P11) :- infer(P1, A, P11)
。
infer(alt(P1, P2), A, P21) :- infer(P2, A, P21)
。
|
| 并行 |
infer(par(P1, P2), A, par(P11, P2)) :- infer(P1, A, P11)
。
infer(par(P1, P2), A, par(P1, P21)) :- infer(P2, A, P21)
。
infer(par(P1, P2), A, par(P11, P21)) :- infer(P1, A1, P11), infer(P2, A2, P21), sync(A1, A2, A)
。
|
| 管道 |
infer(pipe(P1, P2), A, pipe(P11, P2)) :- infer(P1, A, P11), inPorts(P2, IP), not(member(A, IP))
。
infer(pipe(P11, P2), A, P22) :- infer(P1, A, P11), infer(P2, A, P21), outPorts(P1, OP1), member(A, OP1), inPorts(P2, IP2), member(A, IP2)
。
|
我们还使用
trace/2
子句来确定一个顺序是否被组件接受:
7.3 实现约束
为了实现约束,我们添加了以下规则:
在下面的示例中,我们在进程
p1
上施加约束
C1
,并展示整体系统的执行。
7.4 实现变换
我们还实现了操作符的变换。
transf/4
子句接受一个表达式和两个操作符,并生成一个将第一个操作符替换为第二个操作符的表达式。它实现了操作符
[[op1/op2]]
。为了调用该子句,我们定义了
go6/0
子句,其结果如下:
process(p11, call(pressure, [a,b,c], [])).
constraint(constraint1, alt(seq(a, seq(b, seq(c, null))), seq(b, seq(a, seq(c, null))))).
apply_constraint(P, C) :-
process(P),
constraint(C),
infer(constraint(P, C), A, Result),
write(A), write('->'), write(Result), fail.
?- apply_constraint(P11, constraint1).
a->constraint(call(pressure, [b,c], []), seq(b, null))
b->constraint(call(pressure, [a,c], []), seq(a, null))
comp(p, par(call(p1, [a,b], [c,d]), call(p2, [c,e], [g,h]))).
execute(P) :-
comp(P, Process),
infer(Process, Action, Next),
write(Action), write('->'), write(Next).
?- execute(p2).
a->par(call(p1, [b], [c,d]), call(p2, [c,e], [g,h]))
b->par(call(p1, [a], [c,d]), call(p2, [c,e], [g,h]))
c->par(call(p1, [a,b], [c,d]), call(p2, [e], [g,h]))
e->par(call(p1, [a,b], [c,d]), call(p2, [c], [g,h]))
?- comp(p, B), trace(B, [b,a]).
Result: par(call(p1, [], [c,d]), call(p2, [c,e], [g,h]))
infer(seq(A, P), A, P).
infer(constraint(P1, P2), A, constraint(P11, P21)) :- infer(P1, A, P11), infer(P2, A, P21).
在特定动作序列之后,我们还实现了操作符替换(即
[[op1/op2]^n
和
[[op1/op2]_n]
)。
8 控制循环
为了使用我们的形式化模型执行此适应,我们遵循了 MAPE-K 控制循环 [2] 模型(图 1)。该循环的核心包含一个知识数据库以及一组用于执行监控、分析、规划和执行任务的模块。
知识库提供了一组用于适应的规则。它构建了一个包含以下元素的适应模板:
- 名称 :表示上下文的名称。
-
S值
:类别:
<e1:i1, e2:i2, ..., en:in>:类别的可能值区间。 -
区间
:
{类别: <e1:i1, e2:i2, ..., en:in>, ...}:上下文中的一组值区间。每个区间关联一个状态。 - 变换 :在执行动作序列后要应用的转换。
- 变换应用 :必须应用每个变换的状态。它指明了变换的名称以及需要应用的层级。
- 约束应用 :指明状态列表及其对应的约束。
- 约束之后 :指明必须在此序列之后应用约束。如果不存在此指令,则约束在开始时应用。
- 约束 :包含要应用的约束列表。
- 序列 :包含序列列表及其内容。
我们给出一个简化系统的示例,该系统用于治疗患者的体温症状。我们假设患者感觉发烧。如果体温升高显著,则应通过前往附近的医院进行治疗。这种名为
bodytemperature
的系统的简化规范如下:
bodytemperature = pipe(
call(transport, [location], [hospital, no_hospital]),
alt(
par(
call(treatment, [hospital], [normal, fever]),
internal(
call(feverState, [fever], [medication]),
call(normalState, [normal], [no_medication])
)
),
call(no_treatment, [no_hospital], [no_medication])
)
)
我们展示在不进行适应的情况下系统执行的一个片段:
8.1 知识库
知识库包含模板生成规则、其传输以及系统的执行规则(与规划器共享)。在运行
context(bodytemperature)
子句后,我们构建一组规则以表示适应模板。
知识库还包含基于操作符逻辑实现的规则。
?- execute(bodytemperature)
1-location -> …产生的行为:...未显示 \| : 选择一个动作:
1. 1-no_hospital -> …产生的行为:...未显示
2-hospital -> …产生的行为:...未显示 \| : 选择一个动作:
2. 1-normal -> …产生的行为:...未显示...
在
infer/3
子句上。知识库将模板传输给监控器,以便其读取上下文数据。体温上下文的模板是:
知识库通过动态创建适当的规则来定义状态及其关联的区间。它还创建表示约束、变换等的规则:
-
上下文值:
{35, 41} - 状态:
-
低体温 =
[30, 35.5] -
正常 =
[35.5, 37.5] -
发烧 =
[37.5, 42] - 变换:
-
转换1:
par→alt之后:location.hospital.verify.diagnose状态: 正常 -
转换2:
par→pipe之后:location.hospital.verify.diagnose状态: 发烧 - 约束:
-
约束1:
location.hospital.fever.medication.no状态: 发烧 之后:location.hospital.verify.diagnose -
约束2:
location.hospital.normal.no_medication.no状态: 正常 之后:location.hospital.verify.diagnose
state(hypo, [30, 35.5]).
state(normal, [35.5, 37.5]).
state(fever, [37.5, 42]).
constraint(c1, seq(location, seq(hospital, seq(fever, seq(medication, null))))).
constraint(c2, seq(location, seq(hospital, seq(normal, seq(no_medication, null)))))
apply_constraint(c1, fever).
apply_constraint(c2, normal).
transform(t1, seq1, par, alt).
transform(t2, seq2, par, pipe).
apply_transform(normal, t1, 0, 0).
apply_transform(fever, t2, 0, 0).
sequence(seq1, [location, hospital]).
sequence(seq2, [location, hospital]).
8.2 监控器
知识库将模板的内容传递给监控器,以便其过滤数据。它读取数据并确定其相关状态(低体温、正常或发烧)。使用包含温度值的输入数据文件来代替实际传感器。对于读取的每个值,都会设置一个时间戳。我们展示了监控器执行过程的一个片段:
?- monitor.
值: 35.5 – 时间: 1464979388 毫秒,状态: 正常 约束: 约束代码,变换: 变换描述 顺序:实际执行的序列
...
值: 35.4 时间: 1485811766 毫秒,状态: hypo – 新状态。约束: 无,变换: 无 执行序列: [], 变换序列: 无
...
8.3 分析器
分析器确定当前状态,并检测是否存在状态变化。它确定在新状态下要应用的约束和变换。分析器将此变化通知知识库,并更新知识库中的约束和变换。分析器还使用规划器所用的规则来更新知识库。
8.4 规划器
规划器通过使用知识库中存储的规则来组织系统的行为。它执行多项任务。给定一个动作,规划器确定该动作导致的系统行为。例如,执行动作
location
到体温系统的结果是:
pipe(call(transport, [], [no_hospital, hospital]),
alt(par(call(treatment, [hospital], [normal, fever]),
internal(call(feverState, [fever], [medication]),
call(normalState, [normal], [no_medication]))),
call(no_treatment, [no_hospital], [no_medication])))
规划器可能会提交一个动作序列,并验证该序列对于某个组件是否可接受,如果可接受,则获取其执行结果。以下示例显示了序列
[location, hospital]
在体温上的执行。
?- trace(System, [location, hospital], Result).
顺序: [location, hospital]
Result: par(call(treatment, [], [normal, fever]),
internal(call(feverState, [fever], [medication]),
call(normalState, [normal], [no_medication])))
当数据表明系统必须进入新状态时,规划器通过确定以下内容来决定到达该状态的步骤:
(a) 相对于模板中指示的待执行序列以及系统自启动以来已执行的动作序列,需要应用的序列;
(b) 在该序列执行后需应用的约束;
(c) 需要执行的变换以及进行这些变换所应遵循的序列。
8.5 执行器
一旦约束被更新,执行器会应用这些约束以确定当前要执行的动作。在我们的示例中,它使用约束
c1
和
c2
,以及适当的状态:
const(constraint1, seq(location, seq(hospital, seq(fever, seq(s(fever), seq(medication, null))))))
const(c2, seq(location, seq(hospital, seq(normal, seq(nomedication, null))))).
apply_constraint(fever, constraint1).
变换
t1
在正常状态中应用,且包含将并行转换为选择。此变换发生在顺序
location.hospital
之后。通过使用约束和变换,执行器根据系统的当前状态执行系统。
如果发生状态变化,则确定当前约束和变换。在每一步中,它计算动作序列到目前为止已执行的操作并确定更新后的约束。如果没有状态变化,则继续执行。
我们在下面给出一个示例。
示例的模板表明,必须在特定状态和执行特定序列后应用
t1
和
t2
变换。
?- monitor.
值: 35.4 时间: 148539418, 状态: hypo – 新状态。约束: 无, 变换: 无。
1-location-> – 产生的行为: ... \| : 选择一个动作: 1.
值: 37.6. 时间: 1485394190, 状态: 发烧 – 新状态。约束: seq(location,...), 变换: 无。
1-location-> – 产生的行为: ... \| : 选择一个动作: 1.
?- monitor.
值: 35.4. 时间: 1485811766, 状态: hypo – 新状态. 约束: 无, 变换: 无.
执行序列: [], 变换序列: 无
1-location-> – 产生的行为: ... 未显示 \| : 选择一个动作: 1.
值: 37.6. 时间: 1485811770, 状态: 发烧 – 新状态. 约束: seq(location,...)))), 变换: par/pipe
执行序列: [location], 变换序列: [location, hospital]
1-location-> – 产生的行为: ... 未显示 \| : 选择一个动作: 1....
9 结论与未来工作
本文提出了一种用于基于组件的适应的语义模型。该模型基于进程代数来描述系统行为。我们的适应方法包括两个方面:施加约束以限制组件的行为,以及使用变换算子来修改系统架构。我们将所提出的方法集成到 MAPE-K 参考模型中,并对模型以及控制循环的各个阶段进行了模拟。
作为未来的工作,我们计划采用一种基于特征的适应方法,该方法根据上下文激活和停用特征。我们表示组合操作符以及有效的软件产品线配置集。上下文变化模型用于验证这些配置。我们首先从输入源读取上下文数据,然后监控器和分析器根据这些上下文值确定系统的当前状态。各种状态由从上下文派生的上下文规则表示。
变体模型。知识库决定哪种配置是目标配置。然后,它应用一种算法,将这些状态变化映射到当前特征集。在状态变化时,系统指定必须包含在配置中的有效特征。这种特征变化是我们适应方法的核心。
1万+

被折叠的 条评论
为什么被折叠?



