消息传递
@(CAF 文档翻译)
CAF总是异步传递息,进一步说,CAF即不保证消息正确送达,也不保证在分布式环境中送达的顺序.CAF默认使用TCP协议, 但是可以不需要直接链接就可以从一个节点发送消息给另一个节点.在这种情况下,消息发送到中间节点,如果前端节点发送失败就会丢失.同样地转发路径能动态改变路径,因此引起到达顺序不一至.
CAF消息层有三个方式发送消息send
, request
, 和delegate
, 前一个方式简单地将消息放入队列到接收者的邮槽.后两种方式在下面的小节中介绍.
邮槽数据结构
当消息被放入actor
的邮槽队列中,CAF添加元数据和处理路径并包装消息的内容到mailbox_element
(见下图)
发送者作为strong_actor_ptr
存储(见Pointer) 和表示消息的源.消息ID可以是0
或者无效值
或者是标识接收者返回数据给发送者唯一请求的正数.stages
vector保存消息的发送路径.回复消息,i.e.消息处理返回的值在调用stages.pop_back()
后被发送给stages.back()
.这样允许CAF构建任意数量的路径, 如果没有更多的stage剩下,则回复到达发送者.最后content()
通过type-erased tuple
保存消息的内容.
CAF自动创建邮槽数据而且通常对于开发者来说是不可见的.不管怎么样知道消息在内部是如何被处理的帮助我们理解消息传递层的行为.
值得一提的是CAF通常包装邮槽数据和它的内容到单个对象为了减少内存分配.
写时复制
CAF 允许多个actors
隐式的共享消息内容,只要没有actor
修改消息内容.这样允许不需要复制消息内容,就可以发送到所有的订阅相关内容的actors
的组.
每当其它actors
持有消息的引用和如果超过一个消息处理程序获取一个可变的引用,则Actors
拷贝消息内容.
消息类型的需求:
CAF中的消息类型必需满足下列需求:
1. 可序列化或者可视的(见 类型可视(序列化和字符转换))
2. 默认构造函数
3. 拷贝构造函数
如果提供serialize(Serializer&, T&)
or serialize(Serializer&, T&, const unsigned int).
自由函数,则类型是可序列化的.相应地, 如果提供一个inspect(Inspector&, T&)
自由函数,那么类型就是可视的.
需求1是满足需求2的前提, 因为CAF需要能够在创建一个对象之前能够调用serialize
orinspect
时.第三个需求允许CAF实现写时复制(see 写时复制)
默认和系统处理程序.
CAF有三个系统级别的消息类型(down_msg
, exit_msg
, and error
),所有的actor不管当前处于任何状态都应该需要处理.因此, 基于事件的actors有专门的消息处理函数中处理这些消息.此外, 基于事件的actors有个未被匹配消息的处理函数.注意:阻塞的actors没有那些专门的处理函数(见阻塞Actors)
向下处理函数
Actors可以通过调用self->monitor(other)
监控其它actors的寿命.这会造成当其它的actors退出时,CAF的运行系统会发送一个down_msg
.除非通过set_down_handler(f)
提供一个自定义的处理函数,否则Actors会忽略向下消息.f
是一个签名类型为void (down_message)
或者 void (scheduled_actor*, down_message&)
的函数对象.后一个函数签名允许用户使用一个自由函数实现一个向下消息处理函数.
退出处理函数
通过调用self->link_to(other)
一个强生命周期偶合的双向监听被建立.如果this
或者一个exit_msg
退出,会导致系统发送exit_msg
.默认情况除非退出原因是exit_reason::normal
,actors在接收到一个exit_msg
后将停止.这个机制在actor系统中传播失败状态.从一个子系统中一个错误将导致所有连接的actors共同失败.Actors可以通过调用set_exit_handler(f)
重写默认的处理函数, f
为void (exit_message&)
或者 void (scheduled_actor*, exit_message&)
.
错误处理函数
当处理函数返回一个error
,Actors发送错误消息给其它的actor.与退出消息相似,错误消息通常造成接收的actor终止.除非通过set_error_handler(f)
一个自定义的函数被设置, f
是签名为void (error&)
或者 void (scheduled_actor* error&)
的函数对象.另外,request
的第二个参数来设置一个错误处理函数为一个指定的请求(见请求的错误处理).如果request
没有错误处理函数,则默认的处理函数会被使用.
默认处理函数
每当一个actor的行为没有被匹配那么默认的处理函数将被调用.Actors可以调用set_default_handler
改变默认的处理函数.期望的函数签名是result<message> (scheduled*,message_view&)
,忽略self
指针.默认的处理函数能回复消息或者导致运行系统跳过输入消息而允许一个actor去处理它在后来的状态.CAF提供下列内置的实现:reflect
,reflect_and_quit
,print_and_drop
, drop
和skip
.前两个的目的是用来调试和测试和允许一个actor去简单地返回一个输入.接下来的两个函数终止包括或未包括打印一个事先警告的未预料的消息.最后skip
忽略一个输入消息在邮槽中.默认为print_and_drop
.
请求
CAF的一个主要的特性是它能够通过类型系统联合输入和输出类型.举例来说,一个typed_actor<replies_to<int>::with<int>>
本质上同一个函数.它接收一个int
类型作为输入和返回另一个int
.CAF通过该功能来简单的从消息处理函数的结构创建一个响应消息.允许CAF去匹配请求到响应消息和提供一个方便的API用这种风格通讯.
发送请求和处理回复
Actors通过调用request(receiver, timeout, content...)
来发送请求消息这个函数返回一个允许actor去设置一个只处理一次回复消息的处理函数的中间件.基于事件的actor使用request(...).then
或者request(...).await
.前一个异步处理常规的actor的一次性请求和回复.后一个阻塞常规的actor行为直到以后进先出的顺序来处理回复到达.阻塞的 actors总是使用request(...).receive
, 阻塞直到一次性处理函数被调用.Actors接收一个sec:request_timeout
(见系统错误代码)错误消息(见).Actors如果发生一个超时则接收到一个sec::request_timeout
(见系统错误代码)错误消息(见错误处理).用记可以通过设置infinite
做到无限超时.只建议使用在接收者为本地时.
在下列样例中,我们使用简单的cell actors展示终端通讯.
using cell = typed_actor<reacts_to<put_atom, int>,
replies_to<get_atom>::with<int>>;
struct cell_state {
int value = 0;
};
cell::behavior_type cell_impl(cell::stateful_pointer<cell_state> self, int x0) {
self->state.value = x0;
return {
[=](put_atom, int val) {
self->state.value = val;
},
[=](get_atom) {
return self->state.value;
}
};
}
第一部分的代码说明基于事件actors如何使用then
或者await
.
void waiting_testee(event_based_actor* self, vector<cell> cells) {
for (auto& x : cells)
self->request(x, seconds(1), get_atom::value).await([=](int y) {
aout(self) << "cell #" << x.id() << " -> " << y << endl;
});
}
void multiplexed_testee(event_based_actor* self, vector<cell> cells) {
for (auto& x : cells)
self->request(x, seconds(1), get_atom::value).then([=](int y) {
aout(self) << "cell #" << x.id() << " -> " << y << endl;
});
}
第二部分的代码展示了一个阻塞的actorreceive
的使用.注意阻塞的actors没有特定的错误处理函数因此要求传递一个错误处理函数的回调来处理返回消息.
void blocking_testee(blocking_actor* self, vector<cell> cells) {
for (auto& x : cells)
self->request(x, seconds(1), get_atom::value).receive(
[&](int y) {
aout(self) << "cell #" << x.id() << " -> " << y << endl;
},
[&](error& err) {
aout(self) << "cell #" << x.id()
<< " -> " << self->system().render(err) << endl;
}
);
}
我们产生5个cells和参数为0, 1, 4, 9, 和16
vector<cell> cells;
for (auto i = 0; i < 5; ++i)
cells.emplace_back(system.spawn(cell_impl, i * i));
当传递cells
vector到我们三个不同的实现,我们观察三个的输出,我们的waiting_testee
actor将总是打印
cell #9 -> 16
cell #8 -> 9
cell #7 -> 4
cell #6 -> 1
cell #5 -> 0
这是因为await
将一次性处理了函数压入栈中,按照后进先出的顺序来处理来到的回复消息.
multiplexed_testee
实现无法确定打印的顺序.回复消息以任意的顺序到达,并且立即执行.
最后,blocking_testee
实现总是打印:
cell #5 -> 0
cell #6 -> 1
cell #7 -> 4
cell #8 -> 9
cell #9 -> 16
两种基于事件的actors
设置一系列的一次性处理函数,然后从实现函数中返回来处理所有的请求.与之相比,阻塞的函数在发送其它请求之前将等待一个回复到达.
请求内的错误处理
请求机制允许CAF明确地关联请求和回复消息.同样适用于回复一个错误消息.因此,CAF允许添加一个错误的处理函数做为then
和await
的第二个可选参数(这个参数对于receive
是强制性的).如果没有定义这样的处理函数,默认的处理函数(见错误处理函数)被使用调用.
以下例子,我们考虑一个简单的返回一个除零的错误.这个例子使用一个自定义的错误类别(见错误)
enum class math_error : uint8_t {
division_by_zero = 1
};
error make_error(math_error x) {
return {static_cast<uint8_t>(x), atom("math")};
}
using div_atom = atom_constant<atom("div")>;
using divider = typed_actor<replies_to<div_atom, double, double>::with<double>>;
divider::behavior_type divider_impl() {
return {
[](div_atom, double x, double y) -> result<double> {
if (y == 0.0)
return math_error::division_by_zero;
return x / y;
}
};
}
当发送一个请求给除法器, 我们使用一个自定义的错误处理报告一个错误.
scoped_actor self{system};
self->request(div, std::chrono::seconds(10), div_atom::value, x, y).receive(
[&](double z) {
aout(self) << x << " / " << y << " = " << z << endl;
},
[&](const error& err) {
aout(self) << "*** cannot compute " << x << " / " << y << " => "
<< system.render(err) << endl;
}
);
延迟消息
可以使用delayed_send
发送一个延迟消息.以下的代码展示一个基于时间循环的例子.
// uses a message-based loop to iterate over all animation steps
void dancing_kirby(event_based_actor* self) {
// let's get it started
self->send(self, step_atom::value, size_t{0});
self->become (
[=](step_atom, size_t step) {
if (step == sizeof(animation_step)) {
// we've printed all animation steps (done)
cout << endl;
self->quit();
return;
}
// print given step
draw_kirby(animation_steps[step]);
// animate next step in 150ms
self->delayed_send(self, std::chrono::milliseconds(150),
step_atom::value, step + 1);
}
);
}
委派消息
Actors
能通过delegate
传递请求的任务.这能够使委派消息的接收者去回复通常是从消息处理函数简单地返回一个值使得源发送者能收到回复.下列图解展示了请求委派从actor
B到actor
C.
A B C
| | |
| ---(request)---> | |
| | ---(delegate)--> |
| X |---\
| | | compute
| | | result
| |<--/
| <-------------(reply)-------------- |
| X
|---\
| | handle
| | response
|<--/
|
X
从消息处理函数返回delegate(...)
的结果,下列代码演示压制隐含的回复消息和允许编译器检查当使用静态的actors
的返回类型.
void actor_a(event_based_actor* self, const calc& worker) {
self->request(worker, std::chrono::seconds(10), add_atom::value, 1, 2).then(
[=](int result) {
aout(self) << "1 + 2 = " << result << endl;
}
);
}
calc::behavior_type actor_b(calc::pointer self, const calc& worker) {
return {
[=](add_atom add, int x, int y) {
return self->delegate(worker, add, x, y);
}
};
}
calc::behavior_type actor_c() {
return {
[](add_atom, int x, int y) {
return x + y;
}
};
}
void caf_main(actor_system& system) {
system.spawn(actor_a, system.spawn(actor_b, system.spawn(actor_c)));
}
消息的优先顺序
默认情况,所有消息有相同的优先级和actors
忽略优先级标志.通过使用priority_aware
模板参数来产生一个带优先级的actors
, 如下面代码所示, 该标志导致actor
使用带优先级的邮槽实现.无法动态修改.
#include "caf/all.hpp"
using std::endl;
using namespace caf;
behavior foo(event_based_actor* self) {
self->send(self, "world");
self->send<message_priority::high>(self, "hello");
// when spawning `foo` with priority_aware flag, it will print "hello" first
return {
[=](const std::string& str) {
aout(self) << str << endl;
}
};
}
void caf_main(actor_system& system) {
scoped_actor self{system};
aout(self) << "spawn foo" << endl;
self->spawn(foo);
self->await_all_other_actors_done();
aout(self) << "spawn foo again with priority_aware flag" << endl;
self->spawn<priority_aware>(foo);
}
CAF_MAIN()