DCR 框架中 Message 类的分析

04 Mar 2022 | code, cpp

DCR是实验室内部使用的分布式计算框架,本文讨论其中自动注册工厂模式的实现方法。

假设有多个线程分别运行着不同的组件,它们之间通过消息Message传输数据。现在消息种类有很多,为了降低耦合度,你可能更倾向于使用消息工厂类MessageFactory,而不是手工显性地创建消息对象。这样只需要调用MessageFactory::build函数,带上你想创建消息类的标识符,就能获得相对应的消息对象:

struct Message { ... };

struct MessageA : public Message { ... };

struct MessageFactory {
    Message* build(const string &name) {
        if (name == "MessageA")
            return new MessageA;
    }
};

这样的话存在一个问题,如果需要创建新的消息类MessageB,那你不得不找到MessageFactory::build函数的位置,添上新的条件判断语句,这也会导致MessageFactory所在的编译单元得重新编译一遍。如果你的代码是以库的形式发布出去,此时用户可能会创建新的消息类,那么目前的MessageFactory::build函数就没办法继续工作了。

这时候你可能会想到用map来替代build函数中的一长串条件判断语句,同时要求用户创建新的消息类后,必须调用MessageFactory::registerMessage进行注册。紧接着,你会写出这样的代码:

struct MessageFactory {
    void registerMessage(const string &name) {
        builders_ = ?;
    }

    Message* build(const string &name) {
        return builders_[name];
    }

    std::map<string, ?> builders_;
};

示例代码中?的部分应该是什么呢?registerMessage如何获得用户新创建的消息类?builders_的值应该是某种能够创建新对象的东西?DCR框架采用的是模板+多态的解决方法。

为了能够创建新的对象,DCR使用多态为builders_的值中加入了一层抽象:

struct MessageFactory {
    ...

    struct AbstractMessageBuilder {
        virtual Message* build() = 0;
    };    
    
    Message* build(const string &name) {
        return builders_[name]->build();
    }

    std::map<string, AbstractMessageBuilder*> builders_;
};

不同的消息类只需创建各自的AbstractMessageBuilder子类并加入builders_中,MessageFactory::build便能够通过调用虚函数AbstractMessageBuilder::build,返回指向不同消息类的Message指针。而这也是MessageFactory::registerMessage的实现方式。

刚刚我们讲到,MessageFactory::registerMessage代码中需要创建AbstractMessageBuilder子类,这样问题来了,如果不同的消息类都调用了MessageFactory::registerMessage,如何创建不同的AbstractMessageBuilder子类呢?

结合之前DCR是使用模板来获得用户新创建的消息类,此时我们意识到,可以使用模板特化来创建不同的AbstractMessageBuilder子类:


struct MessageFactory {
    ...
    template<typename MessageType>
    strcut MessageBuilder : public AbstractMessageBuilder {
        virtual Message* build() {
            return new MessageType;
        }
    };

    template<typename MessageType>
    void registerMessage(const string &name) {
        MessageBuilder<MessageType> *builder = new MessageBuilder<MessageType>();
        builders_[name] = builder;
    }
    ...
};

好了,现在万事大吉,用户只需要在使用MessageFactory::build前调用MessageFactory::registerMessage就可以正常使用这个工厂类了:

// DCR代码
MessageFactory message_factory;

// 用户代码
class UserMessage : public Message { ... };

int main() {
    message_factory.registerMessage<UserMessage>("UserMessage");
    ...
    Message* message_ptr = message_factory.build("UserMessage");

}

看起来还行,但是稍显不足,用户可能忘记调用registerMessage函数。要是registerMessage这个函数能够自动调用就好了,最好是在用户定义新的消息类时,能够插入一些代码来完成这部分工作。你可能会这么写:

// DCR代码
#define DCR_REGISTER_MESSAGE(MessageType) \
message_factory.registerMessage<MessageType>(#MessageType);

// 用户代码
struct UserMessage : public Message { 
    UserMessage() {
        DCR_REGISTER_MESSAGE(UserMessage);
    }
 };

这样要求用户在其新消息类型的构造函数中,必须使用DCR_REGISTER_MESSAGE宏。但是仔细一想,这是不对的。如果用户不实例化UserMessage,那岂不是没有注册消息?我们希望新的消息类型是无论有无实例化,无论有几个对象,都只注册一次。

仔细想想,static成员变量好像能够派上用场,因为同个类的所有对象都共享相同的成员变量。我们只需将DCR_REGISTER_MESSAGE宏的内容放入static成员变量的构造函数中不就可以了,接着再继续用宏进行简化代码:

// DCR代码
template<typename MessageType>
struct DoRegister {
    DoRegister(string name) {
        message_factory->registerMessage<MessageType>(name);
    }
};
#define DCR_MESSAGE_DECLARE_STATIC(MessageType)  \
static DoRegister<MessageType> do_register_;

#define DCR_REGISTER_MESSAGE(MessageType) \
DoRegister<MessageType> MessageType::do_register_(#MessageType);

// 用户代码
struct UserMessage : public Message { 
    DCR_MESSAGE_DECLARE_STATIC(UserMessage);
    ...
 };

DCR_REGISTER_MESSAGE(UserMessage);

这是一种可行的解决方法。而DCR的实现方法比较繁琐,它不是把static成员变量放在用户定义的新消息类中,而是使用模板类RegisterMessageFactory,用户每次定义新的消息类UserMessage都会额外实例化一个含有static成员变量的RegisterMessageFactory<UserMessage>类,精简后如下:

// DCR代码
template<typename MessageType>
struct RegisterMessageFactory; 

#define DCR_REGISTER_MESSAGE(MessageType)                                   \
template<>                                                                  \
struct RegisterMessageFactory<MessageType> {                                \
    struct DoRegister {                                                     \
        DoRegister(const string &name) {                                    \
            message_factory->registerMessage<MessageType>(name);            \
        }                                                                   \
    };                                                                      \
    static const DoRegister do_register_;                                   \
};                                                                          \
const RegisterMessageFactory<MessageType>::DoRegister                       \
    RegisterMessageFactory<MessageType>::do_register_(#MessageType);  


// 用户代码
struct UserMessage : public Message { ... };

DCR_REGISTER_MESSAGE(UserMessage);

这样子,用户定义完新的消息类后,只需要使用DCR_REGISTER_MESSAGE宏就可以将新消息类注册到工厂类MessageFactory中了。这就完了吗?还没有!

我们知道,不同编译单元内的non-local static变量的初始化顺序是不确定的。DCR框架中的消息工厂对象message_factory,与用户代码中RegisterMessageFactory模板类的静态成员变量do_register_是位于不同的编译单元,这意味着有可能do_register_先初始化,调用了还未初始化的message_factory

因此我们可以使用单例模式,把message_factory作为局部静态变量:

MessageFactory* globalMessageFactory() {
    static MessageFactory message_factory;
    return &message_factory;
}

do_register_使用globalMessageFactory()来获得message_factory,确保message_factory先于do_register_被初始化:

#define DCR_REGISTER_MESSAGE(MessageType)                                   \
template<>                                                                  \
struct RegisterMessageFactory<MessageType> {                                \
    struct DoRegister {                                                     \
        DoRegister(const string &name) {                                    \
            globalMessageFactory()->registerMessage<MessageType>(name);     \
        }                                                                   \
    };                                                                      \
    static const DoRegister do_register_;                                   \
};                                                                          \
const RegisterMessageFactory<MessageType>::DoRegister                       \
    RegisterMessageFactory<MessageType>::do_register_(#MessageType);  

到此,DCR框架中的Message类就剖析完毕。想更进一步?如果你觉得模板+宏的写法不够新时代,想要把宏都用template和constexpr换掉的话。我们可以利用C++的新特性和GCC的扩展,写出下面代码:

template <typename T>
constexpr auto getTypeName() noexcept {
    std::string name = __PRETTY_FUNCTION__, prefix, suffix;
    prefix = "constexpr auto getTypeName() [with T = ";
    suffix = "]";
    name = name.substr(prefix.size());
    name = name.substr(0, name.size() - suffix.size());
    return name;
}

template <typename MessageType>
struct RegisterMessage : public Message {
    struct DoRegister {
        DoRegister() {
            globalMessageFactory()->registerMessage<MessageType>(
                getTypeName<MessageType>()
            );
        }
    };
    RegisterMessage() {  do_register_;  }
           
    static inline DoRegister do_register_{};
};

这样的话,用户定义新的消息类的方法如下,有点繁琐

// 用户代码
struct UserMessage : public RegisterMessage<UserMessage> { 
    UserMessage() : RegisterMessage<UserMessage>() {}
    ... 
};

值得注意的是,模板类中的静态成员变量是惰性构造,因此需要手动触发do_register_的初始化。到这里,Message类已经被剖析得清清楚楚了。此外,DCR框架中的MessageHandler也是消息机制的一个重要组成部分,不过原理大致相同,也是模板+多态,可以试着自己分析一下。


Older · View Archive (37)

数据密集型系统应用设计

Designing Data-Intensive Applications 笔记

Newer

Fly Me to Peking