AUTOSAR_EXP_ARAComAPI的5章笔记(7)
☞ 返回总目录
5.3.6 Methods
对于远程服务提供的每种方法,Proxy Class都包含一个特定于该方法的包装类的成员。
在我们的示例中,有三种方法,相应的成员分别名为 Calibrate(类型为 methods::Calibrate)、Adjust(类型为 methods::Adjust)和 LogCurrentState(类型为 methods::LogCurrentState)。就像事件类一样,代理类所需的方法类是在一个特定的命名空间 methods 中生成的,而这个 methods 命名空间包含在代理命名空间内。代理中的方法成员用于调用我们的代理所连接的可能是远程服务实例所提供的方法。
让我们来看一下示例中生成的方法类 —— 在这里我们选取 Adjust 方法:
class Adjust {
public:
/**
* 对于所有的输出参数和非空返回参数
* 会生成一个包含该非空返回值和/或输出参数的封闭结构体。
*/
struct Output {
bool success;
Position effective_position;
};
/**
* \brief 此操作将会调用该方法。
*
* 通过操作符,通信管理将会进行调用并返回一个 future(异步结果),它允许调用者获取方法的结果。
*
* \param[in] target_position 参考服务描述。
*
* \return 一个包含 Output 结构体的 future。
*/
ara::core::Future<Output> operator()(const Position &target_position);
};
所以这个方法包装类并不是那么复杂。它仅仅由两部分组成:一个内部结构(struct Output)定义,它汇总了该方法所有的输出(OUT )/ 输入输出(INOUT )参数;还有一个括号运算符重载,使类看起来像函数,用于调用服务方法。
该运算符将服务方法的所有输入(IN )/ 输入输出(INOUT )参数作为输入(IN)参数包含在内。这意味着在抽象服务方法描述中的输入输出(INOUT )参数在 ara::com API 中被拆分成一对输入(IN)和输出(OUT)参数。
对一个服务方法(不是 “单向方法”)调用的返回值是一个ara::core::Future,其中模板参数是内部结构的类型,这个内部结构汇总了该方法的所有输出(OUT )参数。在下一小节中将详细介绍这个 ara::core::Future。
5.3.6.1 One-Way aka Fire-and-Forget Methods
在介绍“普通” 方法提供的功能之前,我们在此简要介绍一下 “单向方法”,其实,我们在上一节中已经提到了这个术语。 ara::com 支持一种特殊的方法,我们称之为 “单向(one-way)” 或 “发射后不管(fire-and-forget)”。从技术上讲,这是一种只有输入(IN)参数的方法 —— 没有输出(OUT)参数,并且不允许引发错误。也无法与服务器进行握手 / 同步!因此,客户端 / 调用者完全无法得知服务器 / 被调用者是否已经处理了 “单向” 调用。
在某些通信模式中,这种尽力而为的方法就完全足够了。在这种情况下,从资源的角度来看,这种 “单向 / 发射后不管” 的语义是非常轻量级的。如果我们查看这种方法的签名,我们会发现它比常规方法的签名更简单。
class LogCurrentState {
public:
/**
* \brief 此操作将会调用该方法。
*
* 通过操作符,通信管理将会进行调用。
*
* 这是一个单向(one - way)方法,所以不会提供任何反馈(返回值/输出参数)。
*/
void operator();
};
5.3.6.2 Event-Driven vs Polling access to method results
与上一节(5.3.5 小节)中描述的对事件数据的访问类似,我们也为基于事件驱动和轮询的方法提供了 API 支持,以用于访问服务方法调用的结果。
这两种方法之间的区别的神奇之处在于返回的 ara::core::Future:ara::core::Future 基本上是 C++11/C++14 std::future 类的扩展版本;详情见 [Specification of Adaptive Platform Core.pdf]。
就像在事件数据访问中一样,这里的事件驱动意味着,一旦方法调用结果到达,方法的调用者(带有代理实例的应用程序)就会被 ara::com实现(Communication Management implementation)通知。
对于 ara::com实现来说,这意味着它必须在幕后设置某种等待机制(WaitEvent),一旦方法结果可用,该机制就会被唤醒,以通知 ara::com用户。那么 ara::core::Future 的不同使用模式是如何工作的呢?让我们更深入地了解一下我们的 ara::core::Future 及其提供的接口:
// 定义一个名为 future_status 的枚举类,基于无符号 8 位整数
enum class future_status : uint8_t
{
ready, // 表示共享状态已准备好
timeout // 表示在指定的超时时间过去之前,共享状态没有准备好
};
// 定义一个模板类 Future,它接受类型参数 T 和类型参数 E(默认为 ErrorCode)
template <typename T, typename E = ErrorCode>
class Future
{
public:
// 默认构造函数,使用 noexcept 修饰表示不会抛出异常
Future() noexcept = default;
// 析构函数
~Future();
// 禁用拷贝构造函数
Future(const Future&) = delete;
// 禁用拷贝赋值运算符
Future& operator=(const Future&) = delete;
// 移动构造函数,使用 noexcept 修饰表示不会抛出异常
Future(Future&& other) noexcept;
// 移动赋值运算符,使用 noexcept 修饰表示不会抛出异常
Future& operator=(Future&& other) noexcept;
/**
* @brief 获取值
*
* 此函数的行为应与相应的 std::future 函数相同
*
* @returns 类型为 T 的值
* @error 域:错误 通过 Promise::SetError 放入相应 Promise 中的错误
*
*/
T get();
/**
* @brief 获取结果
*
* 与 get()类似,此调用会阻塞直到值或错误可用。不过,此调用永远不会抛出异常
*
* @returns 包含值或错误的 Result
* @error 域:错误 通过 Promise::SetError 放入相应 Promise 中的错误
*
*/
Result<T, E> GetResult() noexcept;
/**
* @brief 检查 Future 是否有效,即是否具有共享状态
*
* 此函数的行为应与相应的 std::future 函数相同
*
* @returns 如果 Future 可用则返回 true,否则返回 false
*/
bool valid() const noexcept;
/**
* @brief 等待值或错误变为可用
*
* 此函数的行为应与相应的 std::future 函数相同
*/
void wait() const;
/**
* @brief 等待给定的时间段,或者直到值或错误变为可用
*
* 此函数的行为应与相应的 std::future 函数相同
*
* @param timeoutDuration 要等待的最大持续时间
* @returns 表示是超时还是值可用的状态
*/
template <typename Rep, typename Period>
future_status wait_for(const std::chrono::duration<Rep, Period>& timeoutDuration) const;
/**
* @brief 等待直到给定的时间,或者直到值或错误变为可用
*
* 此函数的行为应与相应的 std::future 函数相同
*
* @param deadline 要等待的最晚时间点
* @returns 表示是否到达时间或值是否可用的状态
*/
template <typename Clock, typename Duration>
future_status wait_until(const std::chrono::time_point<Clock, Duration> deadline) const;
/**
* @brief 注册一个可调用对象,当 Future 准备好时调用该对象
*
* 当调用 @a func 时,保证 get()和 GetResult()不会阻塞
*
* @a func 可以在此调用的上下文中调用,也可以在 Promise::set_value()或 Promise::SetError()或其他地方的上下文中调用
*
* @a then 的返回类型取决于 @a func 的返回类型(也称为延续)
*
* 设 U 为延续的返回类型(即与 std::result_of_t<std::decay_t<F>(Future<T,E>)>等效的类型)
* - 如果 U 对于某些类型 T2、E2 是 Future<T2,E2>,那么 @a then()的返回类型是 Future<T2,E2>。这称为隐式 Future 展开
* - 如果 U 对于某些类型 T2、E2 是 Result<T2,E2>,那么 @a then()的返回类型是 Future<T2,E2>。这称为隐式 Result 展开
* - 否则它是 Future<U,E>
*
* @param func 要注册的可调用对象
* @returns 延续结果的新 Future 实例
*/
template <typename F>
auto then(F&& func) -> Future<SEE_COMMENT_ABOVE>;
/**
* @brief 返回异步操作是否已完成
*
* 如果此函数返回 true,则保证 get()、GetResult()和等待调用不会阻塞
*
* 如果 valid()返回 false,则此函数的行为未定义
*
* @returns 如果 Future 包含值或错误则返回 true,否则返回 false
*/
bool is_ready() const;
};
ara::core::GetResult () 在 ara::core::Future 对象中返回结果(Result)或者错误(Error),并且不会抛出异常。get () 返回相应的 ara::core::Future 并且 / 或者抛出异常。
有关自适应平台中错误处理方法的详细文档,请参阅 [Specification of Adaptive Platform Core.pdf] 中 “错误处理” 章节。
下面是使用 “基于异常” 的方法来同步调用一个方法的示例:
using namespace ara::com;
int main() {
// 一些获取句柄的代码
//...
RadarServiceProxy service(handle);
// 通过服务的 Calibrate 方法(传入 myConfigString 参数)调用,得到一个包含 Calibrate::Output 类型的 Future 对象
Future<Calibrate::Output> callFuture = service.Calibrate(myConfigString);
/**
* 现在我们进行一个阻塞式的 get()调用,当接收到结果(有效结果或者异常)时,该调用将会返回。
*
* 如果 Calibrate 方法可能抛出异常并且服务端已经设置了异常,那么这个异常将会通过 get()抛出
*/
Calibrate::Output callOutput = callFuture.get();
// 对得到的 callOutput 进行处理...
return 0;
}
简而言之:从开发人员的角度来看,对服务方法的同步调用仅仅包含()操作符调用语法,以及随后对返回的 future (同步 / 异步结果)进行阻塞式的 get () 调用。除了从对 get () 的阻塞调用中恢复执行之外,一旦方法结果可用,用户还有其他方式从通信管理实现中获得通知:
-
“wait” 的变体,ara::core::Future 从 std::future 中继承了这些变体。它们主要提供了阻塞等待 future 完成的功能。
-
通过 then()注册一个回调方法。这是对 std::future 的扩展之一;详见 [Specification of Adaptive Platform Core.pdf] 。
简单的无参数的 wait() 变体与 get () 具有相同的阻塞语义,即阻塞直到 future 有一个有效的结果(值或异常)。 “wait” 的变体,你要么给出一个持续时间( wait_for())要么给出一个目标时间点( wait_until()),如果 future 有一个有效的结果或者超时 / 最后期限限制已到,它们就会返回 —— 因此它们都返回 future_status 以区分这些情况。 获得 future 结果(有效或异常)通知的最后一种可能性是通过 then()注册一个回调方法。这是 ara::core::future 对 std::future 的扩展之一。
如你所见,到目前为止我们讨论的(并且在示例中部分展示了)获取 future 方法结果的所有可能性 —— 阻塞式的 “get”、所有 “wait” 变体和 “then”—— 都是事件驱动的。也就是说,方法结果(或错误)的到达事件要么导致被阻塞的用户线程恢复执行,要么导致对用户提供的函数的调用!
当然,有些情况下,ara::com用户根本不希望他的应用程序(进程)被某些方法调用返回事件激活!想象一个典型的实时(RT)应用程序,它必须完全控制自己的执行。我们已经在事件数据访问的上下文中讨论了这个实时 / 轮询用例(小节 5.3.5.3)。对于方法调用,同样的方法也适用!
因此,我们预见到了与 ara::core::future 相关的以下使用模式:通过()操作符调用服务方法后,只需使用 ara::core::Future::is_ready () 进行轮询,以查看方法调用是否已经完成。此调用被定义为非阻塞的。当然,它可能涉及一些系统调用 / 上下文切换(例如查看一些内核缓冲区),这并不是无代价的,但它不会阻塞!在ara::core::Future::is_ready () 返回 true 之后,可以保证下一次对 ara::core::Future::get () 的调用不会阻塞,而是在没有错误的情况下立即返回有效的值,或者在出现错误的情况下抛出异常。
5.3.6.3 Canceling Method Result
在某些情况下,你可能已经通过()操作符调用了一个服务方法,该方法返回了一个 ara::core::future ,但你已经不再关心结果了。
甚至可能你已经通过 ara::core::Future::then() 为它注册了一个回调。与其放任不管并 “忽略” 这个回调,你应该明确地告知通信管理(Communication Management)。
这样可以释放资源,并避免在绑定实现层面产生不必要的处理负载。要告知你不再对方法调用结果感兴趣,只需让 ara::core::Future 超出作用域,这样它的析构函数(destructor)就会被调用。
调用 ara::core::Future 的析构函数是向绑定实现发出的一个信号,表示不应再调用为此 future 注册的任何回调,方法调用结果的预留 / 分配内存可以被释放,并且方法结果的事件等待机制应该停止。
显然,为了触发对析构函数的调用,你可以让 future 超出作用域。根据应用程序架构的不同,这可能并不可行,因为你可能已经将返回的 ara::core::Future 分配给了某个作用域更大的变量。
为了解决这个问题, ara::core::Future 是可默认构造的。因此,你可以像下面的示例所示,简单地用一个默认构造的实例覆盖变量中返回的 ara::core::Future :
using namespace ara::com;
// 定义一个存储 Calibrate::Output 类型的 Future 对象
Future<Calibrate::Output> calibrateFuture;
int main() {
// 一些获取句柄的代码
//...
RadarServiceProxy service(handle);
// 调用服务的 Calibrate 方法(传入 myConfigString 参数),并将返回的 Future 对象赋值给 calibrateFuture
calibrateFuture = service.Calibrate(myConfigString);
/**
* 发生了一些状态改变,使得 calibrate 方法的结果变得多余了...
*
* 我们通过将变量重置为一个新的默认构造的 Future 对象来强制删除(原来的 Future 对象)
*/
calibrateFuture = Future<Calibrate::Output>();
// 继续执行其他操作...
return 0;
}