从这篇blog开始,我们来分析百度CyberRt这个高性能的分布式通信中间件

CyberRt的源码组成如下:

image-20231123135714615

base文件夹是Apollo开发的高性能基础库,我们先从这个文件夹里的代码看起:

image-20231123202400187

从文件命名中可以看见实现了与线程相关的如线程池、锁、无锁队列、哈希表等等基础组件,我们就开始从零造轮子吧

1.c++前置知识

std::nothrow

(std::nothrow) 是在C++中用于进行内存分配时的一种选项。通常,当你使用 new 运算符创建对象时,如果内存分配失败,new 会抛出 std::bad_alloc 异常。但是,当你希望在分配失败时不抛出异常,而是返回一个空指针,你可以使用 (std::nothrow) 作为参数传递给 new

具体来说,使用 (std::nothrow) 会使得 new 在分配失败时返回一个空指针而不是抛出异常。这样,你可以在分配失败时通过检查返回的指针是否为空来处理错误,而不必使用异常处理机制。

以下是一个示例:

#include <iostream>

int main() {
// 尝试分配一个非常大的数组,可能导致分配失败
int* arr = nullptr;
arr = new(std::nothrow) int[1000000000000];

if (arr == nullptr) {
std::cout << "Memory allocation failed." << std::endl;
} else {
std::cout << "Memory allocation successful." << std::endl;
delete[] arr; // 记得释放内存
}

return 0;
}

在这个例子中,我们尝试分配一个非常大的整数数组。由于这个数组可能太大而无法成功分配,我们使用 (std::nothrow),这样 new 在分配失败时会返回一个空指针。在分配失败的情况下,我们打印一条错误消息。这样,我们可以通过检查指针是否为空来处理内存分配失败的情况,而不必处理异常。

std::once_flag && std::call_once

在C++中,std::once_flag 是一个用于确保只执行一次代码的标记。它通常与 std::call_once 函数一起使用,以确保其中的代码只会在多线程环境下被执行一次。

多线程环境中,多个线程可能同时尝试执行某个特定的代码块,但有些代码块可能只需要执行一次。这时,就可以使用 std::once_flagstd::call_once 来确保代码块只会在第一次调用时执行,而后续调用会被忽略。

在C++中,std::once_flag 是一个用于确保只执行一次代码的标记。它通常与 std::call_once 函数一起使用,以确保其中的代码只会在多线程环境下被执行一次。

多线程环境中,多个线程可能同时尝试执行某个特定的代码块,但有些代码块可能只需要执行一次。这时,就可以使用 std::once_flagstd::call_once 来确保代码块只会在第一次调用时执行,而后续调用会被忽略。

template<typename Callable, typename... Args>
void call_once(std::once_flag& flag, Callable&& func, Args&&... args) {
// 加锁
std::unique_lock<std::mutex> lock(flag.mutex);

// 双检锁,检查是否已经被执行过
if (!flag.called) {
// 调用传入的函数
func(std::forward<Args>(args)...);

// 设置标志,表示函数已经执行过
flag.called = true;
}
}

在这个简化的示例中,std::once_flag 包含一个互斥锁(mutex)和一个布尔标志(called)。当第一个线程调用 std::call_once 时,它会获得互斥锁,检查标志。如果标志为假,表示函数还没有执行过,于是调用传入的函数,然后设置标志为真,释放互斥锁。如果标志为真,说明函数已经执行过,不再重复执行。

通过使用互斥锁和双检锁的技术,std::call_once 在多线程环境下能够保证传入的函数只执行一次,同时尽可能地减小了锁的开销。需要注意的是,尽管双检锁可以提高性能,但也需要小心处理一些细节,以防止出现竞态条件和内存可见性的问题。在实践中,使用现代C++标准库提供的 std::call_once 是比手动实现更为安全和简便的选择。

std::enable_if

typename std::enable_if<!HasShutdown<T>::value>::type

这行代码使用了std::enable_if,它是一个模板元编程工具,用于在编译时根据条件启用或禁用模板的某个部分。

  • std::enable_if<HasShutdown<T>::value>:这是一个模板元编程的条件,它基于 HasShutdown<T>::value 的值。如果 HasShutdown<T>::valuetrue,那么这个表达式的结果是 std::enable_if 的一个特殊的内部类型,否则没有这个内部类型。
  • typename:这是告诉编译器,后面的 std::enable_if<HasShutdown<T>::value>::type 是一个类型名,而不是一个成员变量或函数。

所以,整个表达式的意思是:如果 HasShutdown<T>::valuetrue,则这是一个有效的类型;否则,这个表达式没有有效的类型。

2.宏分析

2.1 特征判断宏

DEFINE_TYPE_TRAIT定义在base/macros.h中:

#define DEFINE_TYPE_TRAIT(name, func)                     \
template <typename T> \
struct name { \
template <typename Class> \
static constexpr bool Test(decltype(&Class::func)*) { \
return true; \
} \
template <typename> \
static constexpr bool Test(...) { \
return false; \
} \
\
static constexpr bool value = Test<T>(nullptr); \
}; \
\
template <typename T> \
constexpr bool name<T>::value;
  1. DEFINE_TYPE_TRAIT(name, func) 定义了一个宏,该宏接受两个参数,name 是要定义的类型特征结构体的名称,func 是要检查的成员函数的名称。这个宏在内部定义了一个名为name的结构体,此结构体中提供了两个函数模板。

  2. template <typename T> struct name { ... }; 定义了一个模板结构体,该结构体接受一个类型参数 T。实际上这个宏就是用来检查传入的这个T是否包含有func这个函数,借助了name这个结构体来实现

  3. name结构体的内部有一个数据成员就是valuevalue的类型是static constexpr bool,是一个静态常量,这里使用constexpr来声明此变量是为了让编译器在编译的时候就把value的值计算出来,value的值就代表了传入的这个T是否有func这个函数,这个值的计算是通过调用Test<T>这个函数模板来实现的,传入的参数是nullptr

  4. Test<T>这个函数模板有一个泛化的版本和一个特化的版本,首先来看特化的版本

    template <typename Class>                             \
    static constexpr bool Test(decltype(&Class::func)*) { \
    return true; \
    }

    通过decltype去识别&Class::func,如果说&Class::func是存在的话,那么decltype(&Class::func)推断出的类型就是一个指向成员函数函数的指针,这就说明了class 存在一个名为func的函数,后面这个*号我没想明白为啥要加估计是为了告诉编译器传入的参数是一个指针类型吧,如果是这样的话代码可以改成下面这个样子更具有可读性:

    template <typename Class>                             \
    static constexpr bool Test(decltype(&Class::func)* ptr) { \
    return true; \
    }

    这样也就解释了为啥后面计算value值得时候传入了一个nullptr作为参数。回到最初,假设我现在传入的这个T这个类没有func这个成员,那么编译器就会去调用下面这个泛化的版本,并且上面那个特化的版本不会报错,这叫做c++的SFINAE特性

    template <typename>                                   \
    static constexpr bool Test(...) { \
    return false; \
    }

当你想要检查一个类型是否有某个成员函数时,你可以使用这个宏。下面是一个简单的例子,假设你想检查一个类型是否有 size 成员函数:

#include <iostream>

// 定义宏
#define DEFINE_TYPE_TRAIT(name, func) \
template <typename T> \
struct name { \
template <typename Class> \
static constexpr bool Test(decltype(&Class::func)*) { \
return true; \
} \
template <typename> \
static constexpr bool Test(...) { \
return false; \
} \
\
static constexpr bool value = Test<T>(nullptr); \
}; \
\
template <typename T> \
constexpr bool name<T>::value;

// 使用宏定义类型特征结构体
DEFINE_TYPE_TRAIT(HasSize, size)

// 一个示例类
struct MyClass {
void size() const {}
};

int main() {
// 使用类型特征检查类型是否有成员函数
std::cout << "HasSize for MyClass: " << HasSize<MyClass>::value << std::endl; // 输出 1 (true)

// 另一个示例类,没有 size 成员函数
struct AnotherClass {};

std::cout << "HasSize for AnotherClass: " << HasSize<AnotherClass>::value << std::endl; // 输出 0 (false)

return 0;
}

在这个例子中,DEFINE_TYPE_TRAIT(HasSize, size) 定义了一个名为 HasSize 的类型特征结构体,用于检查类型是否有 size 成员函数。然后,通过 HasSize<MyClass>::valueHasSize<AnotherClass>::value 分别检查了 MyClassAnotherClass 是否有 size 成员函数。根据定义,MyClasssize 成员函数,而 AnotherClass 没有,因此输出结果分别为 1(true)和 0(false)。

2.2 单例宏

代码在:cyber/common/macros.h中:

DEFINE_TYPE_TRAIT(HasShutdown, Shutdown)

template <typename T>
typename std::enable_if<HasShutdown<T>::value>::type CallShutdown(T *instance) {
instance->Shutdown();
}

template <typename T>
typename std::enable_if<!HasShutdown<T>::value>::type CallShutdown(
T *instance) {
(void)instance;
}

// There must be many copy-paste versions of these macros which are same
// things, undefine them to avoid conflict.
#undef UNUSED
#undef DISALLOW_COPY_AND_ASSIGN

#define UNUSED(param) (void)param

#define DISALLOW_COPY_AND_ASSIGN(classname) \
classname(const classname &) = delete; \
classname &operator=(const classname &) = delete;

#define DECLARE_SINGLETON(classname) \
public: \
static classname *Instance(bool create_if_needed = true) { \
static classname *instance = nullptr; \
if (!instance && create_if_needed) { \
static std::once_flag flag; \
std::call_once(flag, \
[&] { instance = new (std::nothrow) classname(); }); \
} \
return instance; \
} \
\
static void CleanUp() { \
auto instance = Instance(false); \
if (instance != nullptr) { \
CallShutdown(instance); \
} \
} \
\
private: \
classname(); \
DISALLOW_COPY_AND_ASSIGN(classname)
  1. 首先定了两个很有意思的函数模板

    DEFINE_TYPE_TRAIT(HasShutdown, Shutdown)

    template <typename T>
    typename std::enable_if<HasShutdown<T>::value>::type CallShutdown(T *instance) {
    instance->Shutdown();
    }

    template <typename T>
    typename std::enable_if<!HasShutdown<T>::value>::type CallShutdown(
    T *instance) {
    (void)instance;
    }

    DEFINE_TYPE_TRAIT(HasShutdown, Shutdown)结合上面所讲的这个宏定义了一个名为HasShutdown的结构体用于判断传入的模板T这个类是否包含Shutdown这个成员函数,然后又使用了c++的SFINAE特性做了一个很有意思的操作

    • typename告诉了编译器后面std::enable_if<HasShutdown<T>::value>::type这玩意儿是一个类型,上面这两个CallShutdown的函数模板入参都是一样的,唯一有区别的就是这个返回值的类型。

    • 我们来看看std::enable_if的源码

      // Primary template.
      /// Define a member typedef @c type only if a boolean constant is true.
      template<bool, typename _Tp = void>
      struct enable_if
      { };

      // Partial specialization for true.
      template<typename _Tp>
      struct enable_if<true, _Tp>
      { typedef _Tp type; };

      可以看见c++官方对std::enable_if做了两种定义,一个泛化版本和一个特化版本

      std::enable_if<true, T> //用的是偏特化的版本
      std::enable_if<false, T> // 用的是泛化的

      假设HasShutdown<T>::value的值为true,那么上面两个CallShutdown函数就变成了如下两种情况:

      typename std::enable_if<true>::type     
      typename std::enable_if<false>::type

      很明显std::enable_if<false>::type会走泛化版本,此时std::enable_if内部是没有type这个成员的,由于SFINAE特性的存在,此时并不会报错,所以编译器只会去走std::enable_if<true, T> ,但是这里我很奇怪的一点是typename std::enable_if<true>::type这里没有传第二模板参数呀,按我的理解是不是应该``typename std::enable_if<true,T>::type这样子用,我不是很明白,我想了一下,我先姑且认为编译器会先去做泛化版本的判断,此时由于泛化版本中 template<bool, typename _Tp = void>,第二模板参数被默认成了void此时编译器发现前面这个bool值为true然后就去走下面这个特化版本,在特化版本中就会定义type的值,此时就是:typedef void type`了。

      同理假设HasShutdown<T>::value的值为fasle,上面两个CallShutdown函数就反过来了,当去调用CallShutdown函数时就会走下面这个版本。

    综上所述,上面这两个函数模板就是用于检测类型 T 具有 Shutdown 成员函数,那么这个函数将被启用,否则将被禁用。在启用的情况下,它调用 instance->Shutdown()。类型 T 不具有 Shutdown 成员函数的情况。在这种情况下,这个函数什么都不做。

  2. DISALLOW_COPY_AND_ASSIGN 宏用于禁止拷贝和赋值操作。它通过将拷贝构造函数和拷贝赋值操作符声明为 delete 来阻止对象的拷贝和赋值。这通常用于单例模式等情况,以确保对象只能有一个实例。

    #define DISALLOW_COPY_AND_ASSIGN(classname) \
    classname(const classname &) = delete; \
    classname &operator=(const classname &) = delete;
  3. DECLARE_SINGLETON 宏用于声明一个单例模式的类。具体来说,它包含以下功能:

    • Instance(bool create_if_needed = true) 函数用于获取单例对象的实例。如果单例对象尚未创建,它将使用 std::call_oncenew 运算符创建一个实例。这确保在多线程环境下仅执行一次对象的创建操作。
    • CleanUp() 函数用于清理单例对象。它调用 CallShutdown 函数来执行对象的清理操作。
    • classname() 构造函数声明为 private,确保类的实例只能通过 Instance 函数创建。
    • DISALLOW_COPY_AND_ASSIGN(classname) 用于禁止拷贝和赋值操作,将默认构造也声明成私有的,以确保类的唯一性。
    #define DECLARE_SINGLETON(classname)                                      \
    public: \
    static classname *Instance(bool create_if_needed = true) { \
    static classname *instance = nullptr; \
    if (!instance && create_if_needed) { \
    static std::once_flag flag; \
    std::call_once(flag, \
    [&] { instance = new (std::nothrow) classname(); }); \
    } \
    return instance; \
    } \
    \
    static void CleanUp() { \
    auto instance = Instance(false); \
    if (instance != nullptr) { \
    CallShutdown(instance); \
    } \
    } \
    \
    private: \
    classname(); \
    DISALLOW_COPY_AND_ASSIGN(classname)

3.参考链接