在过去的两年时间里,我一直在用C++11写分布式数据库。在分布式系统中,远程方法调用是一个大麻烦。能不能像本地方法一样调用远程方法?
能不能以异步的方式调用远程方法而调用线程不阻塞?能不能广播调用远程方法?能不能自动将异常信息带到客户端来处理?
于是我写了一个RPC框架pioneer,支持以下特性:
1.针对集群来设计
2.和本地函数几乎一样的调用方法
4.支持同步调用和异步调用
5.支持有返回值/无返回值,对无返回值的调用进行优化
6.支持一对一、一对多、多播、广播、可靠广播调用
7.支持异常处理
8.不考虑跨语言
在上一篇文章中,我介绍了在C++11中,如何借助std::tuple和可变模板参数来序列化一个函数,并且给出了一个通用实现和测试代码:
实现细节见:https://github.com/galaxyeye/atlas/blob/master/atlas/serialization/function.h
测试程序:https://github.com/galaxyeye/atlas/blob/master/libs/serialization/test/function.cpp
使用atlas::serialization::function,可以很轻松地将一个函数序列化,反序列化以及反序列化后执行。
其秘诀在于使用了三个威力强大的武器:std::tuple, std::function和可变模板参数。
那么有几个问题:
1. 如何序列化std::tuple? 标准库中并没用对std::tuple的输入输出实现,也就是说,标准库中没有实现:
template<typename... Args>
std::ostream& operator<<(std::ostream& os, const std::tuple<Args...>& t);
和:
template<typename... Args>
std::istream& operator>>(std::istream& is, std::tuple<Args...>& t);
boost::serialization中也没有对应实现。唉,得自己动手了。
2. 我们找到了序列化可变参数包的方法,就是引入一个中间变量std::tuple来保存这个参数包,那么我们知道函数function, 和一个参数包tuple,怎么样把tuple里的参数解出来供函数function调用?
对于第一个问题,通常有两种解决方法:第一种是{我的解决方法},另一种是~{我的解决方法}。
我的解决方法纯粹使用函数进行编译期模板推导,代码平白如话:
template<size_t idx, typename Archive, typename ... Elements>
void aux_serialize(Archive& ar, std::tuple<Elements...>& t, single_parameter_pack_tag) {
ar & std::get<idx>(t);
}
template<size_t idx, typename Archive, typename ... Elements>
void aux_serialize(Archive& ar, std::tuple<Elements...>& t, not_single_parameter_pack_tag) {
ar & std::get<idx>(t);
aux_serialize<idx + 1>(ar, t, atlas::is_last_parameter<idx, Elements...>());
}
template<typename Archive, typename ... Elements>
void serialize(Archive& ar, std::tuple<Elements...>& t, last_parameter_tag) {
ar & std::get<0>(t);
}
template<typename Archive, typename ... Elements>
void serialize(Archive& ar, std::tuple<Elements...>& t, not_last_parameter_tag) {
aux_serialize<0>(ar, t, std::false_type());
}
boost兼容的序列化函数:
namespace boost {
namespace serialization {
template<typename Archive, typename ... Elements>
Archive& serialize(Archive& ar, std::tuple<Elements...>& t, const unsigned int version) {
atlas::serialize(ar, t, atlas::is_single_parameter_pack<Elements...>());
return ar;
}
} // serialization
} // boost
完整的代码在这里。这里用到几个编译期函数:
is_single_parameter判断一个参数包是否为单个参数
is_last_parameter判断一个参数是否为参数包中最后一个参数
用这套方案,可以解决很多编译期问题,譬如打印出一个std::tuple,代码几乎一模一样。
其他的解决方案,一般是基于class的模板参数推导,不赘述。
对于第二个问题,facebook的folly提出来一个方案,apply_tuple。folly::apply_tuple是这么用的:
int x = atlas::apply_tuple(std::plus<int>(), std::make_tuple(12, 12));
assert(x == 24);
你可以看到,这就是给定一个函数,以及包含函数的参数的打包的std::tuple,怎么调用这个函数。所以在前面的文章里的function_wrapper的operator就可以这么实现:
template<typename Res, typename ... Args>
class function_wrapper<Res(Args...)> {
public:
Res operator()() const { return apply_tuple(_f, _args); }
public:
std::function<Res(Args...)> _f;
std::tuple<Args...> _args;
};
至此,一个可序列化的函数包装器就成型了。