官网文档 Modeling Real-Time Applications in RSARTE 翻译

Modeling Real-Time Applications in RSARTE

参考:https://rsarte.hcldoc.com/help/index.jsp?topic=%2Fcom.ibm.xtools.rsarte.webdoc%2Fusers-guide%2Foverview.html

本文档介绍了使用Rational Software Architect Real Time Edition (RSARTE)设计实时应用程序时涉及的概念,包括建模这些应用程序时使用的统一建模语言(UML)的子集。此外,通过称为UML-RT的UML实时配置文件引入了一些其他概念,进一步限制和正式化这些模型的结构和行为。最后,它包括一个名为RT services library的运行时库中存在的概念,其中一些可用于模型中。

本文档假定读者具有UML 2和实时应用程序的基本了解。该文档是针对RSARTE 11.1进行的最后一次更新。其中所有的屏幕截图都是在Windows平台下捕获的。

介绍

与其他类型的应用程序相比,实时应用程序具有特殊的特点。例如,实时应用程序通常是复杂的、事件驱动的、有状态的、资源高效的和分布式的。RSARTE的目的是促进实时应用程序的建模和开发。这是通过以下方式实现的:

  1. 定义较小的UML子集,即“UML的RT子集”。
  2. 通过配置文件(称为UML-RT)引入新的实时特定概念。
  3. 支持自动转换符合上述限制的模型,以产生高效的目标代码,例如C++。
  4. 提供一个运行时库(称为RT services库),该库与生成的和手写的代码一起可以编译成可执行的实时应用程序。

从工具的角度来看,RSARTE是在Eclipse上面的扩展。通常它是安装在Eclipse for C/C++ Development发行版上的,这意味着它包含了一个完整的C/C++开发环境(CDT)。然而,它也可以用作更通用的建模工具,事实上它支持完整的UML 2标准。这意味着,对于那些只想使用较小的RT子集来设计应用程序的人来说,RSARTE可能有点困难。本文档旨在通过描述UML的RT子集、UML-RT配置文件和库来帮助使用者。本文档旨在涵盖所有设计人员必须熟悉的概念,以便能够使用RSARTE创建用于实时应用程序的模型。

应该提到,UML的其他部分也可能对建模实时应用程序很有用。例如,在分析阶段和系统建模中,许多其他来自UML的语言构造很有用,比如用例图和时序图中的语言构造。本文档不涉及这些语言构造,因此主要面向使用RSARTE来设计和开发转化为实时应用程序代码的RT模型的用户。

UML的RT子集

在RSARTE中构建实时应用程序模型时,使用了相对较少的概念:

  • 类型概念
    Capsules
    被动类
    协议
    枚举
    接口
    数据类型(用于类型别名)
  • 分组概念
  • 关系概念
    泛化
    依赖
    关联
  • 结构概念
    属性
    端口
    连接器
  • 行为概念
    操作
    事件
    状态机
  • 其他
    构件

此外,RT模型包含的行为代码通常是C++。这种代码出现在模型中各个允许行为代码的地方,例如在操作的主体中、转换的效果中以及状态的进入/退出行为中。但是,行为代码还用于表达式,例如转换保护和属性默认值。此外,行为代码中可用的类型在模型中也经常使用,包括基本类型(int、bool等)和外部数据类型(例如手写的C ++类)。在本文档中使用示例时,我们将使用C ++。但是,应该指出,RSARTE还支持C代码。

本章的其余部分更详细地介绍了上述概念

Capsule

Capsule是实时模型的基本构建块。它们代表了封装数据、结构和行为的模型系统内的单位。Capsule 是一个活动的 UML 对象,这意味着它有自己独立的控制流。在 RT 模型中,Capsule 被赋予了«Capsule»的 stereotype(类型标识符),因此它是一个类。
在这里插入图片描述

“就像普通的UML类一样,capsule可以有属性和操作。然而,与常规类相比,capsule有一些限制,使它们适用于实时应用程序。最重要的限制是,capsule实例的引用不应当像普通类实例引用一样,在应用程序中随意传递。所有capsule实例的引用都由底层运行时系统严格管理,可以避免在实时应用程序中可能出现的并发问题。实际上,这个限制意味着以下几点:”

  1. capsules实例总是由运行时库中的服务创建,而不是通过直接使用new操作符创建。创建一个capsules的过程称为化身,capsules实例被称为capsules的一个化身。capsules实例通常存储在另一个capsules的part属性中,此类part属性称为capsules部件。
  2. 尽管capsules可以拥有公共非静态操作和属性,但不推荐从capsules外部访问这些元素,因为这需要保持对capsules实例的引用。但可以在不持有capsules实例的情况下访问静态操作和属性。
  3. 访问capsules实例的正常途径是向其发送消息。然而,消息不能直接发送到capsules实例,因为这也需要保持对它的引用。相反,消息是通过特定的端口发送,并通过连接器和转发端口路由,直到到达接收方capsules实例。在capsules上定义的端口,也称为服务端口,定义了capsules的主要外部接口,即它可以发送和接收哪些事件。

capsules可以包含一个复合结构,使用复合结构图进行定义。复合结构定义了包含在capsules中的capsules部件。它还可以通过连接器定义capsules端口如何连接到这些capsules部件上的其他端口。这种连接结构定义了到达capsules服务端口的消息路由。有关此内容的更多信息,请参阅复合结构。

capsules还可以包含行为,以状态机的形式进行定义,使用状态机图进行描述。状态机中的操作代码可以访问在capsules上定义的属性和操作。有关capsules状态机的更多信息,请参阅状态机。

如上所述,一个capsules具有自己独立的控制流。更精确地说,每个带有状态机的capsules实例都有自己的控制流,被称为逻辑控制线程。可以让同一个处理线程(被称为物理线程)驱动多个逻辑线程,但这完全透明,不会以任何方式影响capsules的设计。实际上,使用Rational Software Architect工具进行实时应用程序设计的好处之一是,可以轻松更改逻辑线程映射到物理线程,而不会影响模型的

需要注意的是,由于使用类似C++的操作代码,当然可以编写代码来获得一个capsules实例并将其引用传递到应用程序中。然而,如果这样做,就必须非常小心。将这样的引用设置为其他逻辑线程中的代码可访问可能会导致运行时问题,因此不建议这样做。

Passive Class 被动类

并非在实时应用程序中的所有对象都需要自己的逻辑线程。许多对象只是简单的被动数据对象,始终在另一个逻辑线程的上下文中运行。为了对这些对象进行建模,通常使用普通的 UML 类。为了将它们与capsules区分开来,后者也是类,通常使用被动类或数据类这个术语。

被动类没有capsules所受到的任何约束。然而,这也意味着你必须小心,避免同时从不同的逻辑线程访问相同的被动类实例,因为如果这些逻辑线程映射到不同的物理线程,这可能会导致并发性问题。

除了在 C++ 中实现的常规操作之外,还可以使用状态机定义被动类的行为。有关被动类状态机的更多信息,请参见“被动类状态机”。

通常,让被动类实例在逻辑线程中由capsules进行管理是一个很好的做法。如果遵循这个建议,给一个被动类实例一个capsules的引用并不是一个问题,因为所有访问该引用的代码都将在同一个逻辑线程中运行。通过引用管理capsules,被动类实例可以调用capsules上的操作,这在向capsules的端口发送事件等情况下非常有用。

如果您想要向被动类传递capsules实例的引用,建议让capsules实现一个仅公开被动类需要调用的操作的接口。这样,您可以更清楚地定义capsules向其被动类公开的功能,避免它们意外使用capsules实现的其他部分。

模板类

被动类可以通过模板参数进行参数化。这使得该类更加通用,以便可以在需要略微不同行为的不同上下文中使用。有两种类型的模板参数:

  1. 类型模板参数
  2. 非类型模板参数
    该类的模板参数是形式化模板参数,并且在使用模板类时(也称实例化),需要提供相应的实际模板参数。
    对于类型模板参数,可以使用任何类型(例如一个类)作为实际模板参数。对于非类型模板参数,例如可以使用一个常量属性作为实际模板参数。

除了类之外,您还可以为一些其他类型定义模板参数(接口和类型别名)。操作也可以具有模板参数。可以为capsules定义模板参数,但这需要提供创建该capsules实例的代码(因为在此类capsules的具现化时需要提供实际模板参数)。如上面的脚注所述,这样的代码只能放置在运行时系统已知的某些位置。还有一些限制,即在capsules实现中如何引用这样的模板参数。

在图表中,模板参数显示在符号的右上角。这里是一个例子(“att”是非类型模板参数,“T”是类型模板参数)
在这里插入图片描述

以下是两个使用模板参数时的示例:
• 带有属性的类,在不同的上下文中需要使用不同类型的属性。可以使用类型模板参数(type template parameter)为属性指定类型。

• 带有数组属性的类,在不同的上下文中需要使用不同大小的数组。可以使用非类型模板参数(non-type template parameter)指定数组的大小,创建模板实例时可以给该参数指定一个整数类型的常量。

协议(Protocol)

协议定义了可以通过端口发送和接收的消息类型。传入消息由输入事件描述,传出消息由输出事件描述。因此,协议是端口用户之间的一种通信契约。协议被用作所有具有相同契约的端口的类型。

在RT模型中,协议是由«Protocol»构造型进行了构造型的UML协作图(UML collaboration)。在类图中,协议可以使用类似类的符号进行可视化,如下面的例子所示:
在这里插入图片描述

在这个例子中,我们可以看到capsule“Adder”有一个名为“request_port”的端口,其类型为协议“PI_REQ”。该协议有两个事件:一个名为“compute”的输入事件和一个名为“result”的输出事件。这告诉我们,如果我们要使用“request_port”与Addercapsule实例通信,则我们唯一可以向它发送的事件是“compute”。它还告诉我们,我们通过“request_port”从Addercapsule实例接收到的唯一事件是“result”。

协议是从一种特定端口的视角定义的(它继承了该端口的类型)。因此,在一般情况下,您可能需要为模型中存在的每个端口定义一个协议。然而,一个非常常见的模式是通信发生在两个端口A和B之间,端口A可以接收的事件恰好是端口B可以发送的事件。同样,端口A可以发送的事件恰好是端口B可以接收的事件。对于这种常见情况,定义一个协议就足够了(称为二进制协议,因为它涉及两个端口),该协议继承了两个端口的类型。然后,其中一个端口被设置为共轭(conjugated),这意味着输入事件和输出事件的集合交换了,因此指定为输入事件的实际上变成了输出事件,指定为输出事件的实际上变成了输入事件。以下是一个例子:
在这里插入图片描述

“request_port"和"compute"的类型都是协议"PI_REQ”。然而,“compute"端口是共轭的,可以通过其名称末尾的”~"符号(以及端口符号)看出来。

使用二进制协议可以减少需要创建的协议数量,同时强调了两个端口参与了相同事件集的交换。

RT Service Protocols

RT服务库提供了一些特殊的协议,您可以在模型中使用这些协议对端口进行类型标记。每个协议都提供特定类型的服务,通过这些协议标记的端口不仅用于向服务库发送事件,还用于从服务库接收事件。此外,RT服务库中与每个服务协议对应的类还拥有提供其他功能的函数。

Exception

这个协议用于处理应用程序中可能引发的运行时异常。Exception协议中的每个in-event都指定了可处理的异常
在这里插入图片描述

请注意,服务库本身不会引发任何异常,因此您的应用程序必须在需要时引发异常。以下是引发 “userError” 异常的示例(“ex” 是 Exception协议类型的端口):


if(!myPort.start().send()) 
ex.userError(RTString("Send on myPort failed.")).raise(); 

要了解有关不同异常事件的更多信息,请查看RT服务库的文档。

External

该协议用于从其他线程(这些线程是外部于RT服务库和从模型生成的代码)向capsules实例发送事件。
在这里插入图片描述

与此协议相对应的 RT 服务库中的类具有用于在端口上启用和禁用外部事件接收的函数。这些函数必须从capsules实例运行的线程中调用。当调用 enable() 后,只能接收一个外部事件,直到调用 disable()。外部线程通过在端口上调用 raise() 函数来发送事件。以下是一个示例:


if (theExternalPort->raise()==0){ 
//fail 
} 
else { 
//pass 
} 

请注意,不能从capsule实例所运行的同一线程调用raise()函数。

Frame

这个协议本身不包含任何事件,但是RT服务库中对应的类提供了多个有用的函数。其中包括capsule实例化的函数。以下是一个将一个capsule实例化到可选的capsule部件“terminal”(“frame”是由Frame协议定义的端口类型)的代码示例:

RTActorId capsule_id = frame.incarnate(terminal); 

有关Frame类可用的函数更多信息,请参阅RT服务库的文档。

Log

这个协议本身不包含任何事件,但是RT服务库中对应的类提供了一些与将文本消息记录到控制台相关的函数。它们特别适用于作为基本的调试或跟踪手段。以下是一个示例("logPort"是由Log协议定义的端口类型):


logPort.log("Initialization completed!");

如需了解Log类可用的更多函数信息,请参阅RT服务库的文档。

Timing

此协议用于实现定时器。通过该协议定义的端口作为定时器,在特定的时间点(绝对时间或相对于当前时间的时间间隔)或者在一定的时间间隔后发送超时事件。
在这里插入图片描述

可以在对应的Timing服务库类中找到设置定时器的函数。下面是一个设置从现在起10秒后到期的定时器("timer"是一个使用了Timing协议的端口)的示例代码:

timer.informIn(RTTimespec(10, 0)); 

请查看RT服务库的文档,以获取更多关于Timing的信息。

Protocol State Machines and Interactions

为了指定协议使用的有效和典型通信模式的示例,可以在协议内部创建状态机和交互。这些协议状态机和交互不定义任何内容,因此在将 RT 模型转换为目标代码时将被忽略。但是,协议状态机和交互仍然可以作为协议的规范和文档而具有价值。

以下是上述“PI_REQ”协议的协议交互示例:
在这里插入图片描述

在不同团队设计两个使用特定协议进行通信的capsule时,协议交互和状态机可以尤其有用。团队可以使用协议中的序列和状态机图来了解其capsule预期的行为,例如预期的消息序列和传递这些消息的数据。这有助于确保通信过程的正确性和有效性。

Package

包是相关的模型元素的分组。包没有语义意义,除了为所包含元素的名称提供范围。然而,请注意,C++转换不支持包作为范围对应于这些元素的C++定义。这意味着您必须为位于不同包中的元素选择名称,以避免它们彼此冲突。确保这一点的方法之一可能是为包含在包中的所有元素使用前缀。有关命名注意事项的更多信息,请参见名称。

每个RT模型都以顶级包为根。该包可以是常规包,也可以是模型包。模型包由内部的小三角形表示。从RT的角度来看,常规包与模型包之间没有区别。下面是一个包含常规包的顶级模型包的示例。
在这里插入图片描述

每个顶级包反过来都包含在一个项目中。项目是Eclipse的概念,与UML无关。Eclipse工作空间可以包含多个项目,其中一些是模型项目,另一些是其他类型的项目。除了模型项目外,另一个重要类别的项目是从模型生成的项目,其中包含生成的目标代码,例如C++。

这是一个模型项目"Pi_mult"及其相应的C++项目"Pi_mult_target"的示例,其中包含从模型项目生成的C++代码。
在这里插入图片描述

Enumeration

枚举是一种数据类型,它包含多个常量。每个常量都可以赋予一个表达式值,这个值是使用动作代码编写的。常量的值应该是整数类型。枚举可以用作属性和操作参数类型。也可以用作事件参数类型,并在荚膜实例之间传递。虽然枚举允许添加操作和属性,但在将 RT 模型转换为 C++ 代码时将被忽略。

这是一个包含三个常量的枚举的示例。由于文本值是使用行动代码表示的,因此它们不会显示在UML图中,但可以在代码视图或代码编辑器中轻松查看和编辑。
在这里插入图片描述

如果生成的代码使用支持 C++ 11 的编译器进行编译,您可以将枚举定义为 “作用域”(scoped),并为其指定实现类型。作用域枚举的好处在于,它们定义了一个命名空间,因此不同枚举中的字面值名称不会冲突。为枚举指定实现类型可在性能方面提供优势,特别是当默认类型(即 int 类型)从实现角度来看并不是最优的情况下。

Interface

接口是对类型的抽象定义,它定义了一组操作。这些操作仅包含一个签名,不能包含实现。类(无论是capsules类还是被动类)可以实现接口。通过实现接口,类必须提供与接口中相同的操作集。这意味着,类的客户端可以通过仅提供对类实现的接口的引用,而不是对类本身的引用,获得对类的有限访问权限。客户端只能调用接口中存在的操作。
考虑以下示例:
在这里插入图片描述

客户端如果有一个对PC实例的引用,可以调用getData()和setData()操作,因为它们都是public的。但是,如果一个客户端只能使用实现了接口Ifc的实例的引用,则只能调用getData()操作,因为在接口中只有这个操作是可用的。因此,接口可以控制向类的不同用户公开的功能。

将接口定义为Capsule要实现的也是很常见的。例如,如果需要将对Capsule实例的引用传递给一个被动类,建议让Capsule实现一个接口,该接口只公开被允许的被动类调用的操作。这样,您可以精确地定义Capsule向被动类公开的功能,并避免在类中意外使用Capsule实现的其他部分。

Data Type (“Type Alias”)

UML数据类型可以用于在模型中表示类型别名。类型别名是C++11的一个概念,允许您为模型或目标C++代码中存在的另一种类型给出一个替代的、更易读的名称。它尤其适用于"非平凡"类型,例如具有模板参数、指针类型等类型。否则,使用这样的类型可能需要输入很多内容,因为可能存在多个模板参数,每个参数都应该在使用该类型的所有地方替换为一个类型或值。

下面是一个类型别名数据类型"IntList"的示例,它为整数列表提供了一个别名名称:
在这里插入图片描述

正如在模板类中提到的,一个类型别名数据类型可以有模板参数,使其更通用。例如,通过将上面的示例中的"unsigned int"替换为一个类模板参数,该类型别名可以表示一个通用的元素列表,每次使用该类型别名时,其类型都会被确定。

没有模板参数的类型别名类似于C/C++中的typedef。如果您的目标编译器不支持C++11,您无法使用类型别名,但仍然可以使用typedef。为了表示模型中的typedef,您可以使用一个常规类,其中将C++ General属性页面中的"Kind"属性设置为"typedef"。在"Implementation Type"属性中指定typedef的类型。
在这里插入图片描述

无论是在使用typedef类还是类型别名数据类型时,将定义实现类型或别名类型的所需头文件包含进来是很方便的。对于上面的两个示例,我们可以通过指定一个"header preface"代码片段来实现:

#include <list>

Generalization

泛化关系用于从一个通用的父类继承结构和行为到专门的子类中。可以将泛化关系用于capsule和被动类,但父类和子类应该是相同的类型,即都是capsule或都是被动类。此外,也可以在接口和协议之间使用泛化关系。

一个专门的子类除了那些被声明为私有的成员,可以访问父类的所有属性和操作。还可以继承端口、组合结构(capsules部件和连接器)以及状态机等其他内容。
泛化关系可以在类图中表示。例如:
在这里插入图片描述

在专门的类中,可以重新定义继承的元素以扩展或修改它。重新定义的元素在左上角标有一个小箭头(例如上面示例中“SubCapsule”中的“proto_1”端口图标)。通常,RSARTE会在需要时自动创建重新定义的元素。例如,在组合结构图中,继承的端口将显示在本地定义的端口旁边。继承的端口使用“dimmed”的形状来区分它们与本地定义的端口。当您修改使用此虚淡形状显示的继承端口时,RSARTE会自动在继承的类中创建重新定义的端口,其中将包含修改。基础capsule中的端口将不会被修改。让我们用一些图片来澄清这一点:
在这里插入图片描述

继承的端口,以dimmed形状显示。它未被修改,因此仅存在于父类capsule中
在这里插入图片描述

已被重新定义的继承端口。它现在同时存在于父类capsule和子capsule中
在这里插入图片描述

本地定义的端口。它只存在于子capsule中。

Generalization of Protocols

除了类和接口之外,泛化关系也可用于协议。子协议将继承父协议中定义的所有事件。以下是一个例子:
在这里插入图片描述

一个类型为 “SubProto” 的端口将同时具有一个输入事件 “parentEvent” 和一个输出事件 “myEvent”。

Promotion and Demotion

在类继承层次结构中工作时,通常情况下你可能需要重构模型。例如,你可能会意识到最初在子类中创建的一个操作实际上更适合属于父类。将元素从子类移动到父类的重构操作

相反于泛化,被称为约束,即将一个元素从超类移动到子类。如果超类有多个子类,则该元素将被复制到每个子类中。

RSARTE 支持通过上下文菜单中的命令对一些元素进行晋升和降级。请注意,在晋升和降级后,您可能需要更改移动元素的名称以避免命名冲突。

Excluding Inherited Elements

可以指定某些继承的元素实际上不应该被继承。当进行这样的操作时,我们说该元素在子类中被排除。被排除的元素将从子类的图表中移除,因为它们不再作为继承元素可用。
在项目浏览器中,一个被排除的元素会用UML-RT属性图中的“排除”装饰符进行标记。这里是一个被排除的端口的示例:
在这里插入图片描述

要将一个元素标记为被排除,可以使用上下文菜单中提供的“排除”命令。对于可以被排除的元素,该命令在上下文菜单中是可用的。在相同的上下文菜单中,您还可以找到“重新继承”的命令,它执行相反的操作,即将一个被排除的元素转换为可再次继承的元素。

请注意,即使一个端口或capsule被排除,代表子capsule的C++ 类中相应的成员变量也不能删除。因此,您不应该重用被排除元素的名称,否则在编译生成的代码时,会出现名称冲突

Inheritance Rearrangement

当从模型中删除一个泛化关系时,RSARTE 会为您提供一个选项,可以自动将所有继承的元素从上一个父类复制到子类中。这个操作称为吸收。
当将一个泛化关系从一个子类移动到另一个子类时,吸收也是很有用的,因为对于前一个子类的影响与删除泛化关系时的影响相同。此外,当移动一个泛化关系时,自动排除新子类中的超类中的所有元素也可能很有用。

当您进行继承重排时,RSARTE 将提供一个对话框,其中包括吸收和排除超类属性的选项:
在这里插入图片描述

Dependency

依赖关系用于表达一个元素在某种意义下依赖于另一个元素。客户元素依赖于供应商元素的方式可以有许多不同的方式。以下是一些例子:

  1. 一个client访问在supplier中定义的属性或操作。
  2. 一个capsule与另一个capsule通信,尽管两个部分之间没有直接连接器。
  3. 一个类型别名数据类型将另一个类型引用为其别名类型。
    有时在应用程序中,依赖关系是对依赖性的非正式描述。然而,有时需要添加依赖项才能获得正确的生成C++文件中的包含或前向引用。例如,如果你有一个 capsule C,其中一个属性的类型为 D 类,你必须从 C 到 D 添加一个依赖项。依赖项上的属性用于控制是否将其翻译为生成的 C++ 头文件中的前向引用或包含。
    在这里插入图片描述

如果类型D不是模型中定义的类型,而是在外部头文件中定义的 C++ 类型,那么依赖关系通常会表现为 C 的头部前缀代码段中的 #include 指示语句。当然,你也可以选择在模型中创建自己的 D 类型表示,并设置属性以使其不生成代码。然后,如果你想在类图中显示这种关系,可以创建一个 D 的依赖项。然而,需要注意的是,在这种情况下,依赖关系纯粹是非正式的,而是 #include 确保生成的代码的正确性。

Association

关联关系用于连接类,包括capsules类和被动类。在运行时,关联关系的意义是关联的类的实例可以互相访问。根据关联关系的各种属性,存在不同的规则来决定实例如何访问彼此。
在有向关联中,只有一个实例可以访问另一个实例。也就是说,有向关联只能在一个方向上进行。例如:
在这里插入图片描述

在这种情况下,A类的一个实例在运行时将拥有对B类实例的引用。

在双向关联中,两个实例都可以相互访问。也就是说,双向关联可以在两个方向上进行导航。例如:
在这里插入图片描述

在这种情况下,A类的一个实例在运行时将拥有对B类实例的引用,而B类实例反过来也将拥有对A类实例的引用(尽管不一定是相同的实例)
关联的导航转化为属性,这些属性位于关联的两个关联类(在双向关联的情况下)或只位于其中一个类(在单向关联的情况下)中。类图中显示的关联的其他属性实际上是这些属性的属性,例如多重性和可见性。有关属性的更多信息,请参见“属性”。

Attribute

类的属性定义了在运行时可以将数据放入类的实例中的插槽。放入插槽的数据类型必须与属性的类型兼容。可以使用来自 C/C++ 的所有类型(包括原始类型,例如 int 或 bool,以及用户定义的类型,例如枚举,类或 typedef)。当然,模型中定义的类型也可以使用,例如被动类、接口、枚举和类型别名数据类型。

属性在类图中显示,可以在类的属性区域中显示,也可以通过关联线表示。这两种可视化方式是等效的。例如,下面这两个类图具有相同的含义:
在这里插入图片描述

如果一个属性具有组合聚合关系,则表示该属性是一个部分。如名称所示,为组合属性分配的类实例与容器类实例具有一种生命周期关系(它是容器实例的一部分)。如果容器类实例被销毁,部件实例也将被销毁。
在这里插入图片描述

如果组合属性的类型是capsule,则该属性是capsule部件。在capsule的复合结构图中可以可视化capsule部件。有关此内容,请参阅“复合结构”。

capsules parts有三种类型:

  1. 固定capsules part。默认情况下,capsule部分是固定的,这意味着它们在创建容器capsule时自动创建,在容器销毁时销毁。默认情况下,固定capsule部分的重复度为1。以下是在复合结构图中显示的固定capsule部分的示例:
    在这里插入图片描述

  2. 可选capsule部分。可选capsule部分与容器capsule没有强生命周期关系。它可以在容器创建之后创建(具体化),并且可能在容器销毁之前销毁。默认情况下,可选capsule部分的重复度为0…1。在复合结构图中,可选capsule部分将被显示如下:
    在这里插入图片描述

  3. 插件capsule部分。插件capsule部分是装载在运行时动态填充capsule实例的占位符。当我们不知道在运行时会放置哪些具体的capsule实例时,插件capsule部分是必需的。当将capsule实例“插入”(也称导入)插件capsule部分时,连接器实例会自动建立,以允许capsule实例参与在这些连接器上可能发生的通信中。稍后capsule实例可以从插件capsule部分中删除(也称驱逐),然后连接器实例将自动删除。随着时间的推移,capsule实例可以在不同的插件capsule部分之间移动,因此在复合结构中扮演不同的角色。

  4. 同一capsules实例可以导入到多个插件capsules部分中。一般来说,capsules实例可以位于任意数量的插件capsules部分中,但至多只能位于一个可选或固定capsules部分中。如果一个端口已经在其当前位置被绑定,则不允许导入capsules实例。默认情况下,插件capsules部分的重复度为0…1。
    在这里插入图片描述

对于可选和插件capsules部分,添加到capsules部分中的capsules实例的实际类型可能与capsules部分的类型完全相同,也可能并非完全相同。capsules部分具有替代类型属性,可以使用属性视图进行编辑。如果此属性为true(默认值),则capsules实例的类型只需与capsules部分的类型兼容即可。如果替代类型为false,则实例的类型必须与capsules部分的类型完全匹配。

如果两个capsules的外部接口兼容,则认为它们具有兼容的类型。这意味着它们的公共服务端口应由相同的协议进行类型定义。需要注意的是,子capsules与其继承的父capsules不一定兼容。考虑下面的例子:
在这里插入图片描述

在这个例子中,虽然"Sub2"继承自"Base",但这两个capsules并不兼容,因为"Sub2"有一个额外的公共服务端口。而"Sub1"capsules与"Base"兼容。
另一种聚合类型是"共享",它可用于由被动类定义类型的属性中。这样的属性在生成的C++代码中将被转换为指针。

Multiplicity

属性创建时设置的默认多重性可以在属性视图的"常规"选项卡中进行更改。
在这里插入图片描述

在下拉菜单中,最常见的多重性选择是0…1和1,但您也可以在文本框中输入任何正数。
如果一个capsule部件被设置为具有不是1的多重性,那么其多重性会显示在其部件符号的左上角。此外,该部件符号将呈现为“堆叠”的外观,以显示它可以容纳多个实例。例如,这是一个具有多重性为的capsule部件。
在这里插入图片描述

由于这个capsules部件是固定的,容器capsulesC的实例化将立即导致23个capsulesD的实例化。当C实例被销毁时,这些23个D实例也将被销毁。
如果我们将这个capsules部件改为可选的,那么多重性必须指定为0…23,因为可选capsules部件的多重性必须包括0(这是使它变成可选的原因)。
在这里插入图片描述

在这种情况下,C的实例化不会自动导致任何D的实例化。相反,您需要使用Frame服务手动将capsulesD实例化到capsules部分中。这里是将15个D实例添加到capsules部分“dR1”的C++代码:


for (int i = 0; i < 15; i++) { 
RTActorId d = frame.incarnate(dR1); 
if (! d.isValid()) 
context()->perror("Incarnation failed!"); 
} 

如果由被动类类型化的属性的多重性不是“None”,相应的C++变量将是一个数组。请注意,“None”多重性与1多重性不同,尽管它们都在属性视图中显示为1(因为在UML中默认多重性为1)。如果将多重性设置为1,则C++类型将是长度为1的数组([1])。

Port

端口是capsules的特殊属性类型。它们是capsule实例的一部分属性,因此它们通过capsules实例拥有,即它们在capsules实例创建时创建,在其销毁时销毁。端口由协议定义类型,指定可以发送到或从端口发送的事件(参见协议Protocol)。
端口有一些属性,这些属性可以使用属性视图进行编辑:
在这里插入图片描述

Service

在这里插入图片描述

服务端口是capsule的外部接口的一部分。完整的服务端口集合构成了capsule的完整外部接口。Service属性默认为true。

Behavior

在这里插入图片描述

行为端口可以从capsules的状态机中路由消息或者将消息发送到状态机中。Behavior属性默认为false。

Conjugated

在这里插入图片描述

共轭端口交换其协议的入事件和出事件的意义。也就是说,出事件可以被发送到共轭端口,而入事件可以被从共轭端口接收。除了其特殊的符号外,共轭端口通过在其名称末尾加上“~”符号来表示。Conjugated属性默认为false。

Wired

有线端口必须通过连接器连接到另一个端口才能发送或接收事件。非有线端口在运行时动态连接到其他非有线端口。非有线端口适用于模拟capsules实例之间的通信路径不是静态已知的情况。有关如何使用非有线端口的更多信息,请参见非有线端口的动态连接。Wired属性默认为true。

Publish

非有线端口可以根据其容器capsules实例在客户端/服务器通信体系结构中所扮演的角色而分为已发布和未发布。有关更多信息,请参见非有线端口的动态连接。Publish属性默认为false。

Notification

如果这个属性为true,RT服务库将在端口连接或断开连接时发送通知事件。这些事件称为rtBound和rtUnbound,详见rtBound和rtUnbound事件。通知属性默认为true。

所有类型的事件消息(除了外部事件消息;详见RT服务协议中的外部协议)都是从发送者capsule实例通过其行为端口发送出去的。然后,这些消息通过连接路径进行路由(对应静态定义的连接器或为非有线端口动态创建的连接)。在此路由过程中,一条消息可能通过许多中继端口,将消息汇集到或从位于capsule部件中的capsule实例传递。最终,该消息到达接收者capsule实例的行为端口,并最终由该capsule实例的状态机处理。
请注意,如果一条消息到达中继端口时没有任何连接,那么发送方到接收方的路径会中断,消息将丢失。这通常表示应用程序设计存在问题。

作为示例,考虑下面的组合结构图:
在这里插入图片描述

如果“C2” capsules的状态机通过其“p2”端口发送消息,则由于这两个端口之间的连接,消息将首先到达“C1” capsules的“p1”端口。然而,正如我们在“C1”的组合结构图中看到的,“p1”端口将传入的消息委派给capsules“C3” 的 “p3”端口。因此,“p1”是一个中继端口。

RSARTE在端口的上下文菜单中提供了一个有用的命令,名为“查找连接的端口”。该命令允许您查找选定端口连接的端口。该命令会询问您是否要遍历中继端口(以找到消息将被发送到的终极端口)或不遍历(仅查找所选端口直接连接的端口)。

在上面的“C1”的组合结构图中,我们还可以看到“q1”和“q3”端口之间缺少连接器。因此,“q2”和“q3”之间的通信路径被断开了,如果通过“q2”发送消息,运行时将会打印错误消息,提示该端口未连接。

就像普通属性一样,端口也可以具有非单一的复数多重性(N>1)。这有助于让多个客户端连接到单个服务器端口。服务器端口的多重性则决定了同时连接的最大客户端数量。具有非单一多重性的端口有时被称为复制端口(因为它们可能包含许多端口“复制品”)。复制因子表示端口实例(即连接)的当前数量。具有非单一多重性的端口的图形符号是端口的“堆栈”。
在这里插入图片描述

通常端口的多重性是在设计时静态指定的。但是,可以通过对端口调用resize() 函数来在运行时更改多重性。这样做可能会为更多的连接提供空间(如果增加了多重性上限),但也可能会导致连接断开(如果减少了多重性上限)

Dynamic Connections for Non-Wired Ports

与有线端口相反,有线端口在基于静态连接器结构创建capsules实例时会自动连接,非有线端口则通过使用RT服务库提供的服务在运行时进行程序连接(和断开连接)。非有线端口的常见用法是用于建模客户端/服务器设计,在此设计中,由服务器提供的共享服务被许多客户端访问。服务器称为发布者,客户端称为订阅者。

订阅者上的非有线端口称为服务访问点(SAP)。这种端口的多重性不超过1。发布者上的非有线端口称为服务提供点(SPP)。这种端口的多重性确定可以连接到SPP的SAP的最大数量,因此通常大于1。SAP端口只能与SPP端口连接,反之亦然。
在这里插入图片描述

每个SPP都由一个名称(称为服务名称)唯一标识,该名称描述了容器capsules通过该特定端口提供的服务。SAP使用此名称连接到SPP。这可以通过RT服务库中提供的名称服务器来实现。SPP使用其唯一的服务名称向名称服务器注册自己。SAP执行查找以查找具有匹配名称的SPP。如果找到匹配的SPP,则建立连接。但是,如果找不到匹配的SPP,则将连接请求排队,直到出现具有该名称的SPP。该方案允许建立连接而不依赖于服务器和客户端capsules的化身顺序。

除非请求的服务名称的SPP尚不可用,否则还存在这样一种情况,即SPP可用,但已经连接到其SAP端口多重性上限指定的最大数量。在这种情况下,连接请求将被排队。如果稍后时间内,已连接的其中一个SAP与SPP断开连接,则可以处理一个待处理的连接请求以连接等待中的SAP。另外一种可能发生的情况是,SPP的端口多重性被动态增加,这会为更多的SAP连接提供空间。

每个SAP发出的连接请求都可以提供一个描述连接的字符串。这个字符串附加在服务名称后面,形成一个注册字符串。它的格式如下:
<service name>:<connection string>
服务名称字符串区分大小写,而连接字符串由服务解释,因此可以具有任何格式。
需要注意的是,非有线端口的连接总是由RT服务库建立。应用程序的责任仅是注册端口,使RT服务库能够执行连接(立即或在稍后的时间点)。同样地,端口可以被注销,这可能会导致现有连接被RT服务库移除。同一个端口可以使用不同的服务名称多次注册自己。在这种情况下,在使用新的服务名称注册之前,该端口将自动取消其先前服务名称的注册。

非有线端口的注册可能有三种可能的方式(通过端口上的一个属性来表示):
自动注册。这是默认的注册方式,意味着端口在其所属的capsules实例初始化时自动注册。使用的服务名称是端口的名称。使用这种注册方式时,端口的Publish属性将用于确定端口是SPP(Publish设置)还是SAP(Publish未设置)。

应用注册。采用此注册方式的端口是通过使用在非有线端口上可用的registerSPP()和registerSAP()函数手动注册的。这些函数以注册字符串为其参数。例如:
sppPort.registerSPP(“myService:/x/y/z”);
sapPort.registerSAP(“myService:/x/y/z”);
使用这种注册方式时,您不需要设置Publish属性,因为上述函数调用是决定端口成为SPP或SAP的依据。

自动注册(锁定)。采用此注册方式时,端口的注册方式类似于自动注册。但另外还有一个"锁定"的注册方式,意味着任何后来尝试注销端口(或使用不同名称注册端口)的尝试都将失败。
对于自动注册的两种方式,可以指定一个不同于端口名称的名称进行注册。用于此目的的属性称为Registration Override。

Composite Structure

一个简单的capsule只处理少量事件时,可能能够使用单个状态机处理所有这些事件。但是,当添加新的端口(或协议类型中现有端口的新事件)时,capsule的接口将增长,状态机也必须随之增长,因为需要处理更多的事件。最终会达到一个点,在这一点上,capsule无法在自己的状态机中处理更多的事件,因为其变得过于庞大或复杂。如果还没有这样做,这就是定义capsule的组合结构的时候了。

capsules的组合结构是使用组合结构图进行编辑的。它显示了capsules的部件和端口以及它们如何通过连接器连接。可以显示三种类型的端口:

  1. capsules的服务端口。这些端口构成capsules的外部接口。它们出现在组合结构图的框架上,以表明它们面向着capsules的环境。
  2. capsules的行为端口。到达行为端口的消息将由capsules的状态机处理。同时,状态机发送给其组合结构的消息也会通过行为端口发送出去。
  3. 在定义capsules部分的capsules中定义的端口。这些端口显示在部分符号的边框上。它们也是服务端口,因为它们是定义了capsules部分类型的capsules的接口的一部分。

组合结构图中显示了两种连接器。

Delegation connectors

委派连接器。这些连接器将服务端口的责任委派给capsules部分上的一个端口。所有到达服务端口的消息都将路由到委派连接器另一端的端口。反之亦然;所有到达capsules部分端口的消息都将路由到服务端口。

Assembly connectors.

这些是其他连接器。它们显示哪些capsules部分进行通信,无论是与其他capsules部分还是通过其行为端口与capsules本身进行通信。在某种意义上,这些连接器“组装”了capsules的完整功能,因此被称为组装连接器。
通过使用委托连接器和capsule部件,可以将复杂的capsule分解,从而将其责任的某些部分委派给一个或多个capsule部件。这些部分可以决定自己处理传入的事件,或者再将它们委托给自己的组合结构的capsule部件。

这是一个capsules的组合结构示例:
在这里插入图片描述

我们可以看到这个capsule有一个服务端口“ext”,它在图框架上显示。一个委派连接器将这个端口与capsule“Adder”的定义的端口“test”相连,这个端口类型为capsule部件“Adder”。图上还显示了两个装配连接器。其中一个连接“request_port”与“computer”,这是在“PiComputer” capsule本身定义的一个行为端口。另一个装配连接器提供一个方式,使“adder”能够通过“multiplier”和“result”端口与“multiplier”进行通信。

由于事件是与capsules实例进行通信的唯一(推荐的)方式,组合图提供了一个有用的视图,展示在特定的抽象级别下可能的通信路径。然而,如果使用了非有线的端口,则可能会在运行时动态建立额外的连接。

Operation

一个类的操作可以包含一个由动作代码组成的方法体,当操作被调用时,这样的方法体将被执行。在 UML 中,这样的方法体被称为OpaqueBehavior。将操作签名与操作实现分离成两个单独的模型元素的做法,对于 C++ 程序员来说应该很熟悉,因为 C++ 也采用了相同的方法来分离函数的声明和实现。

操作可以有参数,这些参数提供了一种将数据传递到其实现体的方式。缺省情况下,参数具有 “In” 方向,这意味着它只能将数据从操作的调用者传递到操作的实现中。UML 也允许您指定 In/Out 或 Out 参数作为传递数据到操作调用者的方式。但是,在一个 RT 模型中,这些方向对生成的代码没有影响,尽管您仍然可以将它们用作一种文档。相反,您必须使用操作语言的机制来实现相同的功能。例如,在 C++ 中,可以使用引用参数(&)来给参数赋值,从而使参数的值可以被调用者获取。

操作也可能具有单个返回参数,它指定了操作调用所计算的值的类型。如果没有返回参数,或者返回参数的类型为"void",则该操作不能在需要值的上下文中调用,例如赋值操作的右侧。

操作可以标记为纯虚操作。这样的操作不允许具有实现体,只是表示具体的子类必须为它们提供一个覆盖实现。因此,接口中的所有操作都是隐式的纯虚操作。包含至少一个纯虚操作的类(或capsules)实际上是一个抽象类,即无法创建此类的实例。在Project Explorer和图表中,纯虚操作以斜体显示,并在其签名后附加了“= 0”的文本。抽象类也以斜体显示。

在这里插入图片描述

在 C++ 中,默认情况下,操作被视为非虚的。要使其成为虚的,必须设置 Virtual 属性。在 C++ RT 模型中,必须使用此属性才能在调用操作时获得多态行为。在子类中重新定义虚操作的操作应标记为 Override。您还可以通过设置 Final 属性来表达操作不能在子类中被重新定义。

如果操作不应修改其对象的任何属性,则应指定为const操作。这对应于C++中的“const”成员函数。
可以在常量表达式中调用(即编译器可以在编译时计算的表达式)的操作应指定为constExpr。这对应于C ++中的“constexpr”成员函数。
操作可以指定为static,这意味着它们的实现不需要对象。因此,静态操作不能访问任何非静态属性或调用任何非静态操作。
如果设置了inline属性的操作将被翻译成C++中的内联函数。内联函数的主体将生成到头文件中,而不是实现文件中。因此,通常需要为模板操作设置此属性,因为大多数编译器要求此类函数的主体生成到头文件中。
触发操作是一种特殊类型的操作,用于触发被动类状态机的转换。有关更多信息,请参见被动类状态机中的状态机。
一个操作可以拥有模板参数,用于为一个或多个参数添加类型。这样的模板参数可以使操作的实现更加通用化,原因与模板类中提到的相同。

Constructor and Destructor

对于 C++ RT 模型来说,能够为类指定构造函数和析构函数非常重要。在模型中,它们被表示为常规操作,但具有某些约束(与 C++ 中相同):
构造函数与其所在类的名称相同,不应该有返回值参数。
析构函数的名称与其所在类的名称相同,但具有“~”前缀。
请注意,“~”前缀的使用在 UML 语法中是有歧义的,其中“~”用于指定包可见性(在 RT 模型中不使用)。为了处理这个歧义,当编辑一个操作的签名时,将“~”作为名称的一部分进行指定非常重要。
在这里插入图片描述

错误!这将创建一个名称为“foo”的操作,并具有包可见性。
在这里插入图片描述

正确!这将创建一个名称为“foo”的析构函数,并具有公共可见性。

Thrown Exceptions

说明:操作可以指定其实现可能抛出的异常类型列表。UML 使用名为“RaisedExceptions”的属性来实现此操作,可以使用属性视图中的“异常”选项卡进行编辑。但是,在从模型生成目标代码时,RSARTE 不支持此属性。相反,您需要将运算参数属性“Is Exception”设置为真来为运算指定异常类型。还需要将操作的属性“Declare Exceptions”设置为真。该属性位于“C++ General”选项卡中。以下示例说明如何操作:
在这里插入图片描述

异常参数的类型是操作将抛出的异常类型。以上操作将转换为以下操作代码:

void foo( void ) throw( int * );

Event

一个事件指定了capsules之间的通信。通信是基于从一个capsules实例发送到另一个capsules实例的消息。您可以将事件视为某些消息类型的规范,就像类是某些实例类型的规范一样。因此,虽然事件是设计时的概念,但消息则是相应的运行时概念。

我们经常说事件被发送和接收。然而,我们所说的发送事件的实际意义是为事件创建一个消息,然后发送该消息。同样地,接收事件实际上意味着我们接收了该事件的消息。

事件属于一种协议,将所有可能到达(in-events)或离开(out-events)由该协议类型化的端口的所有消息分组在一起。在 RT 模型中,输入事件用 «InEvent» 构件化,输出事件用 «OutEvent» 构件化
在这里插入图片描述

与调用操作时一样,可以通过为其事件定义参数来传递消息数据。然而,尽管操作可以有任意数量的参数,但事件最多只能有一个参数。这意味着为了使用消息传递多个数据值,您必须使用数据类对事件参数进行类型化(该数据类可以具有多个属性来保存数据值)。

请注意,RSARTE实际上允许您在模型中使用多个事件参数,尽管在将模型转换为目标代码时仅支持一个事件参数。多个事件参数的使用仅支持规范模型。
要发送事件,您必须指定:

  1. 通过该端口发送消息的端口。 这是发送capsules上的行为端口,并且该端口的类型必须是指定要作为输出事件发送的协议(或者在端口共轭的情况下为输入事件)。
  2. 要随消息发送的数据(如果事件有参数)。
    这里是一个 C++ 代码示例,它向端口“mul”发送一个名为“getIncrement”的事件。消息将携带整数值4作为其数据。

mul.getIncrement(4).send();

值得注意的是,当向消息发送数据时,RT服务库将复制该数据。其原因在于避免发送方capsules实例意外访问已发送到接收方capsules实例的数据对象,这些对象可能在不同的物理线程中运行。如果事件参数数据对象包含指针,则必须小心地实现适当的构造函数、复制构造函数和析构函数,以确保在复制时以线程安全的方式处理该对象。使用具有指向不同capsules实例之间共享的数据的指针的数据类可以增加性能,但也需要极度谨慎,以确保对这种数据进行线程安全的访问。

Priorities

发送到capsules实例的消息并不总是能立即被RT服务库调度执行。capsules实例可能正在执行转移,必须在另一条消息被调度执行前完成。因此,RT服务库维护了一个消息队列,其中等待被调度执行的消息。默认情况下,这些消息按照它们到达的时间排序,这样在队列中放置的第一条消息也是第一条被调度执行的消息。需要注意的是,在实时应用程序中,不能保证消息以它们发送的顺序到达目标capsules实例。因此,消息只能基于它们实际到达接收方capsules实例的时间进行排序。

可以发送具有非默认优先级的消息,以便告知RT服务库在到达时按不同方式对其进行排序。具有较高优先级的消息将排在具有较低优先级的消息之前。可以使用以下优先级级别:

  • Panic级别,是用户定义消息的最高优先级。仅在紧急情况下使用。
  • High级别,是高于普通消息优先级的级别。用于高优先级消息。
  • General级别,是适合大多数消息的默认优先级级别。
  • Low级别,是低于普通消息优先级的级别。用于低优先级消息。
  • Background级别,是最低优先级。用于处理后台类型活动的消息。

除了这五个优先级级别之外,还有两个系统级别优先级比以上所有优先级都要高;System和Synchronous。这些由RT服务库在内部使用,不能在发送用户定义消息时使用。

在实时应用程序中广泛使用消息优先级可能是设计问题的一个指示。建议尽可能坚持使用默认的优先级级别,或者至少避免使用高和低极端优先级以便为未来的设计更改留出空间。

The rtBound and rtUnbound Events

RT服务库定义了两个特殊事件,隐含地适用于任何用户定义的协议:

  1. rtBound
  2. rtUnbound
    RT服务库为这些事件发送消息,以通知capsules实例其一个端口已绑定到另一个端口或者与另一个端口解除绑定。当一个端口被连接(即与另一个远程端口绑定)时,rtBound消息将以系统优先级发送到该端口。这是capsules的有用信息,因为它知道它可以开始通过该端口发送事件。当一个端口断开连接(即解除绑定)时,rtUnbound消息将以后台优先级发送到该端口。在接收到rtUnbound消息后,capsules应该停止在该端口上发送事件。

如果一个端口的Notification属性被设置为false,RT服务库将不会向该端口发送这些通知事件。

State Machine

状态机用于指定类的行为。被动类和capsule类均可以拥有状态机。此外,还可以为协议添加状态机,但这种状态机并不指定任何行为,而是作为协议的一种文档。有关协议状态机和交互的更多信息,请参阅《协议状态机和交互》。

本章我们将重点讨论capsule中的状态机。被动类中状态机的使用在《被动类中的状态机》中有所描述。

状态机由状态、伪状态和转换组成。在capsule实例的生命周期中,状态机会在各种状态之间转换,这是由于行为端口上到达的消息的影响。在状态之间转换时,可能会执行一个或多个操作代码片段。例如,这种代码可以向其他capsule实例发送消息。

capsule实例实例化后发生的第一件事情是执行初始转换。这是从初始伪状态发起的转换。每个状态机都有一个初始伪状态。
在这里插入图片描述

当初始转换运行完成(即当其效果中的代码执行完毕)时,状态机进入与初始转换连接的状态。当进入一个状态时,可能会执行另一个代码片段;即该状态的进入行为。通过状态机图中的图标来表示状态上存在进入行为的情况。如果将光标悬停在该图标上,则可以在弹出窗口中查看该代码。您也可以使用代码视图或代码编辑器查看和编辑代码。
在这里插入图片描述

有时候,当进入状态机中的任何状态时执行公共的进入行为是很有用的。例如,您可能想运行一些跟踪代码,打印进入状态的名称。为此,可以使用一个名为“rtgStateEntry”的面向capsules的状态进入操作。此操作应该是虚函数,并具有以下签名:

rtgStateEntry() : void 

如果定义了这个操作,在状态进入之后、状态自身的进入行为执行之前,它将被调用。因此,一个状态的完整进入行为由“rtgStateEntry”操作和在状态本身上定义的任何进入行为组成。

在一个capsule的行为端口中没有消息到达之前,状态机将保持当前状态。在此期间,它不执行任何操作。当有消息到达时,来自当前状态的一个传出转移可能会被触发。如果发生这种情况,状态的退出行为代码将被执行,随后是转移效果中的代码。最后,将进入转移引导到的状态(这将导致其进入行为代码执行)。在从一个状态转换到另一个状态的整个过程中,即使在一个行为端口接收到一个优先级更高的消息,状态机也不会被中断。这个原则被称为 run-to-completion 原则,它对保证实时应用的完整性非常重要。然而,这个原则也意味着,在状态之间转换时运行的代码(即从状态的退出行为、转移效果到到状态的进入行为)不应该太长,因为在此期间不能在由同一物理线程运行的任何capsule实例中处理任何其他消息。为了确保应用的良好响应能力,因此触发转换的代码应该只执行快速任务。如果必须执行长时间运行的任务,应该由在不同物理线程中运行的单独capsule来处理。

一个状态可能有多个传出转移,并且每个转移都可能有多个触发器。每个触发器指定以下内容:

  1. 触发消息可能到达的行为端口。
  2. 触发转换的消息的事件。此事件在类型指定指定端口的协议中定义,并且应该是一个in-event(除非该端口是共轭的,在这种情况下,它应该是一个out-event)。可以指定所有协议中的in-event都将触发转换。这可以通过使用事件符号’*'(在UML中称为AnyReceiveEvent)来完成。以下是指定触发事件的对话框示例。
    在这里插入图片描述

一个守卫条件。这是一个布尔表达式,用动作代码编写,必须对触发器进行求值,才能触发转换。指定守卫条件是可选的,缺少守卫条件相当于一个计算为true的守卫条件。

触发器可以在状态图中的转换文本标签中显示,但通常会被过滤掉,以避免在图表中添加过多细节。相反,当选中转换时,触发器是通过属性视图查看和编辑的。以下是一个例子:
在这里插入图片描述

触发器守卫条件的实际代码可通过代码视图(或代码编辑器,如果需要更多空间)进行查看和编辑。在上面的例子中,使用了RT Services库中的字面量RTTrue和RTFalse,但也可以使用C++布尔字面值true和false。请注意,守卫条件必须使用return语句返回(即仅输入布尔表达式是不够的)

还可以通过在转换本身上放置守卫条件来为所有触发器指定共同的守卫条件。如果转换上和触发器上都存在守卫条件,那么这些条件都必须满足才能触发触发器以触发转换。

对于一个触发器接收到的事件的转换,其中触发器和转换的守卫条件都被满足,称为已启用。这意味着接收到的消息可能会触发它。然而,正如我们将在分层状态机中看到的那样,对于一个特定接收到的消息可能会有多个已启用的转换。关于哪个启用的转换实际触发的规则在分层状态机中进行了说明。

从一个状态向另一个状态的转换可能涉及一个以上的转换。通常情况下,一整个转换链可能会被执行,其中此链中的第一个转换拥有与接收到的消息匹配的触发器。在UML中,这样的转换链称为复合转换。链中除第一个转换外的所有转换都起源于伪状态,且可能没有任何触发器。RT模型中支持以下种类的伪状态:

  1. 初始伪状态。如上所述,指定状态机的起始点。

  2. 选择伪状态。在状态机中指定一个点,以动态选择决定在复合转换中执行下一个外向转换。选择伪状态具有两个或多个外向转换,每个转换都有一个守卫条件。第一个守卫条件计算为 true 的转换将被执行。建议确保守卫条件是互斥的,这样它们被评估的顺序就不重要了。其中一个守卫条件可以设置为“else”,以表示在其他转换不执行时将执行该转换。例如:
    在这里插入图片描述

  3. 汇聚伪状态。在状态机中指定一个点,多个转换在该点汇聚。汇聚点非常有用,可以在响应不同事件时执行公共转换。例如:
    在这里插入图片描述

在这里,“t1”和“t2”转换可以指定不同事件的触发器,而“t3”转换可能包含一个通用的效果代码,当这些事件的任何一种到达时都会运行它。另一种实现相同功能的方法是将公共代码放在一个操作中,然后从“t1”和“t2”的效果中调用该操作。

  1. 入口点、出口点和历史伪状态。这些被用于层次状态机,并在“层次状态机”中进行了描述。

源状态和目标状态相同的转换称为自转换。
在这里插入图片描述

有两种类型的自转换:

  1. 外部自转换。这是默认设置,意味着在触发转换之前将退出状态,之后再次进入。因此,状态的退出和进入行为都将被执行。
  2. 内部自转换(或简称为内部转换,因为所有内部转换都是自转换)。在这种情况下,当转换被触发时,状态的进入和退出行为都不会被执行。至少有一个内部转换的状态,在其右上角有一个特殊标志。
    在这里插入图片描述

内部转换可以显示在状态符号的一个特殊区域中。要启用此区域,请选择状态符号,然后在属性视图的外观选项卡中进行设置。从那里,您可以使内部转换区域可见。
在这里插入图片描述

切换此区域可见性的更快的方法是双击右上角的图标。下面是具有可见内部转换区域的上述状态符号:
在这里插入图片描述

如果一个状态没有进入行为、退出行为,也没有包含具有进入或退出行为的子状态,则这个状态的外部自转换等同于内部自转换。当您进行选择时,建议使用内部转换来建模这样的转换,因为它会导致具有更少连接线的图表,这些图表既看起来更好,也更容易操作。此外,生成的 C++ 代码通常对于内部转换更有效率,因为对于进入或退出行为调用对应的函数调用可以被省略。

除了内部和外部自转换之外,还有本地自转换,可在分层状态机中使用,以指定应该进入和退出子状态,但源状态不应进入或退出。

Hierarchical State Machine

随着状态机变得越来越大,使用子状态机对状态进行分解变得很重要。包含子状态机的状态称为复合状态,具有复合状态的状态机称为分层状态机。

在状态机图中,复合状态用一个特殊的图标表示,通常在其右下角:
在这里插入图片描述

你可以双击状态符号打开其子状态机(如果尚不存在,则创建一个)。子状态机的图表与顶层状态机图表非常相似。但是,存在一些区别:
进入复合状态的转换显示在子状态机图表的边界上。它们与复合状态的入口伪状态相连接。
在这里插入图片描述

当触发进入复合状态的转换时,将像往常一样进入复合状态。但是,此外,源自子状态机入口点的转换也将执行。这意味着入口点伪状态的入站和出站转换是同一复合转换的一部分。最后,将进入子状态机中的状态。

如果子状态机中的入口点没有转出转换,则将使用深层历史状态进入复合状态,这意味着以前活动状态(及其子状态)将重新变为活动状态。在状态图中,“H*”符号表示该状态具有深层历史状态。
在这里插入图片描述

复合状态的转出转换也显示在子状态机图的边界上,但是它们连接到复合状态的出口点伪状态上。
在这里插入图片描述

当触发导向子状态机的出口点的转换时,复合状态将像往常一样首先退出。然后,另外一个从该复合状态的出口点伪状态发出的转换将执行,这就意味着出口点伪状态的进入和出站转换是同一个复合转换的一部分。最后,将进入封闭状态机中的一个状态。

并非强制通过入口点进入复合状态,或通过出口点退出。转换可以直接结束在状态上,而不使用入口/出口点。考虑以下示例:
在这里插入图片描述

如果转换“t1”被激活,它将导致执行转换“initial_2”,因为它使用入口点进入该状态。然而,如果转换“t2”被激活,则状态将在不使用入口点的情况下进入。在这种情况下,“initial_1”从初始伪状态的转换将执行,然而,正如上面提到的,这仅适用于第一次进入状态时。下一次,不使用入口点进入状态相当于通过在子状态机中没有向外转换的入口点进入它(即使用“深层历史记录”进入它)。建议始终使用入口点进入复合状态,因为这可以更清楚地了解将会发生什么。

如果复合状态具有本地或内部自转换,则可以在子状态机图的边界上显示它们。以下是一个示例:
在这里插入图片描述

一个本地的自我转移不会触发复合源状态的进入或退出行为,但转移也适用于所有包含的子状态,并且这些子状态将像通常一样被进入和退出。如果没有具有进入或退出行为的子状态,本地自我转移可以转换为复合状态的内部转移,而不改变模型的含义。这样做可能会导致一个更简单的图表,更容易查看和编辑。

当在子状态机图的边界上显示内部转换时,使用虚线连接线。这是标准 UML 符号的 RT 扩展。请注意,RSARTE 允许您在状态机图下方或旁边的单独区域中显示内部转换。这使得图表更易读,更易于处理。因此,以下两个图表是等效的。
在这里插入图片描述

当子状态机中的状态处于活动状态时,所有包含的复合状态也处于活动状态。这意味着,我们不应该把状态机看作只有一个活动状态,而应当把它看作是一个活动状态列表。这个列表被称为状态机的活动状态配置。然而,在层次结构中的各个单独的状态机中,在任何时刻最多只能存在一个活动状态。

例如,考虑以下状态机层次结构(其中每个子状态机都嵌入到包含的复合状态符号中):
在这里插入图片描述

当这个状态机开始运行时,三个状态 A、B 和 C 将被依次进入。因此,活动状态配置将为 {A, B, C}。事实上,C 状态不能活动而没有 B 和 A 状态处于活动状态,因为它们是包含它的复合状态。在活动状态配置 {A, B, C} 中,可能会触发转换“t1”。这意味着活动状态配置将变为 {AA},即非复合状态 AA 将是唯一的活动状态。在“t1”效果执行之前,必须退出活动状态配置中的所有状态,即按照特定的顺序运行 C、B 和 A 的退出行为。

在一个分层状态机中,确定在某个活动状态配置中可以触发哪些转换的规则稍微复杂一些,因为还需要考虑包含的活动状态的转换。当分派接收到的消息时,使用以下算法来选择要触发的转换:

如果找到已启用的转换,则会发生以下情况:

  1. 在最内层的活动状态中开始寻找匹配的转换。在此状态的范围内,按顺序评估所有外向转换(包括外部、内部和本地转换)。如果发现启用的转换,则搜索终止并触发该转换。如果找到多个启用的转换,则触发第一个。
  2. 如果在当前状态机范围内没有启用的转换,则在状态机层次结构中继续为下一层范围重复搜索。也就是说,现在将搜索离开包含复合状态的转换。
  3. 如果在到达顶层状态(即直接在capsules状态机中定义的状态)时仍未找到启用的转换,并且其所有外向转换都未启用,则会丢弃当前消息。如果发生这种情况,则不会运行任何代码作为分派此消息的效果,并且活动状态配置将保持不变。这通常(但不总是)是应用程序设计存在问题的迹象。

如果找到启用的转换,则发生以下情况:

  1. 如果启用的转换不是内部转换,则执行所有活动状态的退出行为,从最深的嵌套状态开始,直到启用转换源自的状态。如果启用的转换是内部转换,则不执行退出或进入行为。如果启用的转换是本地转换,则为所有嵌套状态执行退出和进入行为,但不执行启用转换源自的状态的退出和进入行为。
  2. 执行启用的转换。这包括执行从启用转换开始的转换链(复合转换)的所有转换片段。所有这些转换的效果按顺序执行。请注意,链中的一些转换可能导致状态的进入或退出,这意味着可能会执行状态的进入和退出行为。最后,在执行完链中的所有转换后,将到达一个状态。此状态将被进入,成为位于其中的状态机中的活动状态。

随着状态机执行,其活动状态配置会不断变化。有时需要表达一个转换应该返回到完全相同的活动状态配置,即使该转换是外部转换。实现这一目的的便捷方式是使用历史伪状态。
在这里插入图片描述

一个状态机中最多只能有一个历史伪状态,它只允许在子状态机中使用。当一个转换到达历史伪状态时,源状态将再次变为活动状态。如果源状态是复合状态,则所有之前处于活动状态的子状态(直接或间接)也将重新变为活动状态。因此,历史是一个“deep history”,可以恢复整个活动状态配置,而不仅仅是源状态本身。

为了看看历史伪状态如何使用,让我们回到之前的例子。当我们处于活动状态配置 {A, B, C},并且触发了转换“t2”时,我们发现有一个到达历史伪状态的转换。因此,在触发这个转换后,活动状态配置仍然是 {A, B, C}。如果活动状态配置中的状态具有进入行为,则在使用历史伪状态进入这些状态时将像往常一样执行这些行为。

在上面的大部分屏幕截图中,我们使用单独的状态图表来描述子状态机。这是最常见的情况,因为通常需要单独的图表来适应子状态机。然而,正如我们在最近的例子中所看到的,如果子状态机足够小,那么在复合状态符号内部展示它可以是一个替代方法。要做到这一点,您必须配置状态符号以显示区域组件。以下是一个例子:
在这里插入图片描述

这种可视化方法可以带来一些好处,因为它使得沿着复合状态上的进入和退出点遵循转换路径更容易。选择哪种可视化方法对状态机的执行没有语义上的影响。

Deferring and Recalling Messages

有时状态机的设计可能是为了按顺序处理一系列不同的消息。如果在处理这样的消息序列时,另一条消息恰好到达,通常有用的做法是推迟处理该消息,直到剩余的消息序列到达,特别是如果到达的消息是另一个消息序列的起点。使用交替并行处理多个消息序列可能很棘手。

要推迟接收到的消息,请调用它的 defer() 函数。请注意,此调用必须在由 RT 服务库触发的代码中进行,即当接收到的消息已经被 RT 服务库分派时。被推迟的消息被保存在由 RT 服务库维护的推迟队列中。它将一直保留在那里,直到明确地回收它,这可以通过调用 recall() 函数来完成。被回收的推迟消息将从推迟队列移回到消息队列中,以便稍后再次发送。默认情况下,回收的消息会像其他任何到达的消息一样被置于消息队列的末尾。然而,也可以将其回收到消息队列的前面,以便它成为下一个要发送的消息。

下面的图片说明了消息队列和推迟队列以及消息如何在这些队列之间移动:
在这里插入图片描述

请注意,defer queue(延迟队列)中的消息在等待时限方面没有时间限制,您必须确保不会遗忘队列中的任何消息。将消息进行延迟处理不可避免地会使状态机设计更加复杂,而且很容易忘记在必要的情况下重新调用消息。因此,建议谨慎使用消息延迟处理机制。

State Machine in Passive Class

可以使用状态机来定义被动类的行为,这可以作为轻量级的capsule状态机的替代方案。然而,尽管被动类状态机看起来几乎与capsule状态机相同,但它们之间有几个不同之处。

一个重要的区别是,被动类状态机总是在capsule的逻辑线程中执行。这是因为被动类没有自己的逻辑线程。

另一个不同之处是,被动类可能没有端口,这意味着在被动类状态机中的转换不能在接收到消息时触发。相反,转换是通过调用在被动类上定义的操作来触发的。这些操作被称为触发操作。触发操作可以具有输入参数,以允许数据传递到转换代码中,但不允许有返回参数。因此,要将数据从转换传递回调用者,您必须使用诸如C++引用参数的机制。

在RT模型中,触发器操作如下所示:
在这里插入图片描述

触发操作是同步运行的,因此将会阻塞调用者,直到触发的转换运行完毕。
触发操作没有自己的代码主体。如果您选择一个触发操作,Code View将不允许您编辑其代码主体。
就像同一协议事件可以在capsules状态机中触发多个转换一样,在被动类状态机中,同一触发操作可以触发多个转换。
被动类上不存在端口的另一个后果是无法直接从被动类状态机中的代码发送事件。但是,通过在调用触发操作时将capsule实例的引用作为参数传递,转换代码可以调用capsule上的操作,进而像往常一样通过端口发送事件。当然,使用的capsule实例应属于与运行被动类的逻辑线程相同的线程,以避免并发问题。相同的技术也可用于在被动类状态机中访问RT服务库中的服务。服务端口像往常一样在capsule上进行定义,并且通过capsule操作使其可对被动类状态机使用。

如果想要将capsule实例的引用传递给被动类,则建议让capsule实现仅公开被动类需要调用的操作的接口。这样,可以更清楚地定义capsule向其被动类暴露的功能,同时避免意外使用capsule实现的其他部分。

在被动类状态机中,可以使用Final状态
在这里插入图片描述

capsule状态机不支持这种类型的状态。但是,如果使用终止状态,它将非常像一个没有出站转换的常规状态。特别是它不会自动导致被动类实例的销毁(这是终止状态的常规UML语义)。通常最好让负责被动类的capsule实例创建和销毁其实例。

与capsule状态机类似,被动类状态机也可以是分层的,并且可以使用进入/退出点。但是,虽然capsule状态机仅支持深层历史伪状态,被动类状态机还支持浅层历史伪状态。
在这里插入图片描述

使用浅层历史状态,先前活动的状态将再次变为活动状态,但仅在包含历史伪状态的状态机中,而不在激活状态的子状态机中。默认情况下,被动类状态机的初始转换在构建被动类实例时执行。这是因为生成的默认构造函数包含对rtg_init1()操作的调用,该操作包含初始化转换的代码。如果创建自己的构造函数,则必须手动包含对此操作的调用,以确保在调用任何触发操作之前初始化状态机。与capsule状态机相反,被动类状态机不能被继承。

Artifact

在这里插入图片描述

有时,您可能希望在模型中包含C++声明,但这些声明并不适用于从UML元素生成。可能没有与您要包含的代码相对应的适当UML元素,或者您可能已经有了一个要在模型中重用的C++代码片段。在这些情况下,您可以使用“artifact”模型元素作为C++代码的容器。artifact表示一对C++源文件(头文件和实现文件),因此有时也称为文件artifact。

artifact有两个代码片段;一个头文件片段,用于将代码生成到头文件中,以及一个实现文件片段,用于将代码生成到实现文件中。可以使用代码视图或代码编辑器编辑这些代码片段,方式与编辑RSARTE中的所有其他代码片段相同。您放置在artifact中的C++代码将按原样打印为生成的C++代码。

使用artifact在模型中表示C++代码的主要好处是,模型包含了生成应用程序所需的所有内容。您可以创建从其他元素到artifact的依赖关系,以表示包含或前向声明,artifact在需要引用来自模型的元素时也可以有类似的依赖关系。RSARTE还支持其他将C++代码与模型集成的方式(例如外部C++库转换配置),但是当代码需要引用模型中的元素时,特别是推荐使用工件。

Transformation Constraints

RSARTE可以将RT模型转换为目标应用程序代码,例如C++。为了使转换成功,模型必须符合上述的UML中的RT子集。但是,还必须满足一些附加约束条件,以确保生成的代码是良好形式的。这些约束条件在本章中进行了描述。

Names

一般情况下,RT转换在生成的代码中使用模型元素的名称,而不进行任何更改。采用这种方法的好处是,编写使用模型元素名称的操作代码片段变得更加容易。然而,这也意味着您必须确保名称选择得当,以便生成的代码可以编译。UML元素的名称必须遵守以下规定:

  1. 是目标语言(例如C++)中的保留字(例如’switch’或’for’)
  2. 与RT服务库中使用的名称冲突(这可能取决于代码片段所在的上下文,但通常情况下,您不应该使用“rtg”前缀来命名模型元素,因为该前缀通常用于RT服务库中的定义)
  3. 与外部代码中使用的名称冲突(例如包含的C++头文件)

第一条规则通过转换进行检查。以下是一个示例
在这里插入图片描述

16:46:51 : ERROR : CPPModel::inline::inline : Protocol with illegal name not
generated.
16:46:51 : ERROR : CPPModel::inline::inline : This element does not have a legal
C++ name: it will not be generated.

第二个和第三个规则的违规将直到生成的代码编译时才会被检测到。以下是一个示例:
在这里插入图片描述

../Base.h:10:8: error: 'Base::Base' has the same name as the class in which it is declared

另外请注意,RT转换不支持使用UML包来进行名称作用域限定。因此,对于在包中定义的元素,使用前缀来避免与在另一个包中定义的元素发生名称冲突是一个不错的想法。

The UML-RT Profile and Libraries

RT模型应用了UML-RT(“UMLRealTime”)配置文件。最好使用New Model Wizard中的UML Capsule Development模板之一创建RT模型,例如UML Capsule C++ Development Model,这样UML-RT配置文件将自动应用。您可以在Properties视图的Profiles选项卡中检查此配置文件,选中顶级包即可:

在这里插入图片描述

UML-RT配置文件包含一些stereotypes,用于表示RT特定的概念,例如Capsule和Protocol。当需要时,RSARTE会自动应用这些成分,因此无需手动处理UML-RT配置文件。您只需要知道它的存在,并确保永远不要删除配置文件应用程序,因为这将导致所有应用的RT stereotypes都被删除。请注意,它们不可能以简单的方式重新应用,因此永远不要删除UML-RT配置文件应用程序。

RT模型还导入一些模型库。导入的库具体取决于所使用的操作语言,但对于使用C++代码的模型,以下库已导入:
在这里插入图片描述

CppPrimitiveDatatypes

CppPrimitiveDatatypes库包含C++原始数据类型的UML表示形式。例如,您将在那里找到数据类型"bool"和"wchar_t"。 "void"类型也可用于明确指定操作不返回值。类型限定符,如指针(*)或引用(&),在UML中没有表示,但仍然可以直接在使用类型引用的地方使用。例如:
在这里插入图片描述

值得注意的是,大多数可以引用C++类型的地方都可以使用类型补全(Ctrl+Space),因此您不必每次需要使用它们时都浏览C++预定义类型。类型补全只能为模型中定义的类型提供补全(包括由CppPrimitiveDatatypes定义的原始C++类型),但无法提供对模型外的类型的补全,例如在外部头文件中定义的C++类。

RTClasses

RTClasses库包含RT服务库中的多个有用类型。因此,根据目标库的不同,会有不同版本的该库。当使用C++时,以下类型可供在UML模型中使用:
RTActorId, RTBoolean, RTByteBlock, RTCharacter, RTDataObject, RTEnumerated,
RTInteger, RTMessage, RTpchar, RTPointer, RTpvoid, RTReal, RTSequence, RTSequenceOf, RTString, RTTime, RTTimerId, RTTimerspec, RTuchar, RTulong,
RTushort
其中一些只是预定义类型的typedef,而其他一些则代表RT服务库中的类。有关这些类型的更多信息,请参阅RT服务库的文档。
RTClasses库还包含重要的协议,用作将RT服务库中的服务暴露给RT模型的手段。这些协议包括:
Exception, External, Frame, Log, Timing
有关这些协议的更多信息,请参阅RT服务协议。

RTComponents

RTComponents库包含一个转换配置"RTComponent",它代表包含RT服务库实现的外部C++库。因此,大多数RT转换配置都将其作为(显式或隐式的)前置转换配置。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值