具体调试代码参考github:https://github.com/hggzhang/CppTest/tree/master
概述
在程序设计中,我们希望关联程度低的对象之间的联系是"松耦合"的,也即减少直接依赖。一般的做法是使用消息机制进行信息的传递和响应,其中事件系统是其一种常规手段之一,下面我们尝试使用C++实现一个事件系统。
观察者模式
假如你想买一份报纸:
- 不停的去邮局问今天的报纸到了嘛?到了嘛?到了嘛?... (轮询模式)
- 去邮局登记要买今天的报纸,之后就回家;邮局报纸到了之后,邮局按照登记名单送报纸。(观察者模式)
对比这两种方式,相对你而言是不是方法2更加高效?你不必一直在邮局等着一直问报纸到了没,登记信息回家看电视等着就好了。所谓观察者模式基本就是这种思想,其核心要素有:
- 订阅消息:对某个感兴趣的消息A进行登记。
- 发布消息:"相关单位"发布消息A,通知所有登记者该消息内容。
当然,这只是主题思想,在程序上实现观察者模式还需要考虑一些其他因素,下面将会进行介绍。
程序总体设计
参考上述邮局的例子,我们来介绍下程序的核心"角色"(类):
- Event 报纸
- EventBus 邮局
- EventListener 买报纸的人
另外还需要其他辅助
- EventRegister 类似秘书或助手,帮助管理订阅相关事务
总结下各个角色的相关职责划分:
- Event 事件信息载体,比如鼠标移动事件包含{x, y}的坐标
- EventBus 事件总线,管理事件的订阅/发布
- EventListener 事件观察者,包含其关心的事件的类型,事件的响应函数,自身的ID
- ID:由于一个事件可能有多个观察者,用于比较确定我们自身是哪个
- EventRegister 管理EventListener,对事件相关行为进行封装,避免让每个使用者都实现一遍。
程序细节设计
这里结合C++语言特性和程序的特点说明下一些需要注意的细节:
Event
https://github.com/hggzhang/CppTest/blob/master/Program/Event.h
使用多态来实现不同的事件。
CPP
class EventBase
{
public:
virtual ~EventBase() = default;
};
class EventPosition : public EventBase
{
public:
EventPosition(int x, int y) :X(x), Y(y) {}
int X;
int Y;
};
EventBus
https://github.com/hggzhang/CppTest/blob/master/Program/EventBus.h
CPP
class EventBus : public Singleton<EventBus>
{
friend class Singleton<EventBus>;
// ...
}
- Singleton 使用全局单例提高效率
CPP
std::unordered_map<std::type_index, std::list<std::weak_ptr<EventListenerBase>>> subers;
- 使用type_index作为key来存储监听者列表,相对于字符串(需使用者定义)和typeid更加稳定
- 监听者列表使用list容器,添加和删除效率更好
- 使用弱指针weak_ptr来缓存监听者避免循环依赖
CPP
#ifdef ENABLE_EVENTBUS_MULTI_THREAD
mutable std::mutex mutex;
#endif
-
使用编译开关控制线程锁
template<typename EventT>
void Pub(const EventT& event)
{
// prevent infinite recursion
static thread_local int depth = 0;
constexpr int MAX_DEPTH = 10;
// ...
std::vector<std::shared_ptr<EventListenerBase>> validListeners;
// ...
std::type_index typeIndex = typeid(EventT);
// ...
} -
使用模板方法来发布事件,typeid(EventT)计算标识符Key更加高效
-
depth和validListeners副本列表避免循环,如发布事件的回调里油订阅了事件等
EventListener
CPP
class EventListenerBase
{
public:
int ID = 0;
std::type_index Key = typeid(void);
};
template<typename EventT>
class EventListener : public EventListenerBase
{
public:
std::function<void(const EventT&)> callback;
EventListener( std::function<void(const EventT&)> InCB)
:callback(std::move(InCB))
{
Key = typeid(EventT);
};
};
- 使用type_index作为标识符相比string和typename更高效可靠
- 由于事件的监听者可能有多个,使用ID用于标识监听者,方便查找
EventRegister
CPP
class EventRegister
{
private:
#ifdef ENABLE_EVENTBUS_MULTI_THREAD
std::atomic<int> id = 0;
#else
int id = 0;
#endif
public:
std::vector<std::shared_ptr<EventListenerBase>> listeners;
// ...
}
- 封装管理EventListener
- 启用多线程时,需要原子化变量
代码清单
参考github地址:https://github.com/hggzhang/CppTest/tree/master
部分代码在此贴出
Event.h
CPP
class EventBase
{
public:
virtual ~EventBase() = default;
};
class EventPosition : public EventBase
{
public:
EventPosition(int x, int y) :X(x), Y(y) {}
int X;
int Y;
};
class EventKeyPress : public EventBase
{
public:
EventKeyPress(char key) :Key(key) {}
char Key;
};
EventBus.h
CPP
#pragma once
#include <unordered_map>
#include <string>
#include <memory>
#include <functional>
#include <vector>
#include <list>
#include <typeindex>
#include <typeinfo>
#include "Event.h"
#include "TSingleton.h"
#define ENABLE_EVENTBUS_MULTI_THREAD
#ifdef ENABLE_EVENTBUS_MULTI_THREAD
#include <mutex>
#include <atomic>
#endif
class EventBus;
class EventListenerBase
{
public:
int ID = 0;
std::type_index Key = typeid(void);
};
template<typename EventT>
class EventListener : public EventListenerBase
{
public:
std::function<void(const EventT&)> callback;
EventListener( std::function<void(const EventT&)> InCB)
:callback(std::move(InCB))
{
Key = typeid(EventT);
};
};
class EventBus : public Singleton<EventBus>
{
friend class Singleton<EventBus>;
private:
#ifdef ENABLE_EVENTBUS_MULTI_THREAD
mutable std::mutex mutex;
#endif
// we use type_index as the key to store different event types, they more safe than using typeid(T) as string,
// and use weak_ptr to avoid circular reference
std::unordered_map<std::type_index, std::list<std::weak_ptr<EventListenerBase>>> subers;
public:
void Sub(std::shared_ptr<EventListenerBase> lsner)
{
#ifdef ENABLE_EVENTBUS_MULTI_THREAD
std::lock_guard<std::mutex> lock(mutex);
#endif
std::type_index typeIndex = lsner->Key;
if (subers.find(typeIndex) == subers.end())
{
subers[typeIndex] = std::list<std::weak_ptr<EventListenerBase>>();
}
subers[typeIndex].push_back(lsner);
}
void UnSub(std::shared_ptr<EventListenerBase> lsner)
{
#ifdef ENABLE_EVENTBUS_MULTI_THREAD
std::lock_guard<std::mutex> lock(mutex);
#endif
std::type_index typeIndex = lsner->Key;
auto it = subers.find(typeIndex);
if (it != subers.end())
{
auto& vec = subers[typeIndex];
// we need to compare the raw pointer, because the shared_ptr in vec is different from lsner
vec.erase(std::remove_if(vec.begin(), vec.end(),
[&](const std::weak_ptr<EventListenerBase>& wp) {
auto sp = wp.lock();
return !sp || sp.get() == lsner.get();
}),
vec.end());
if (vec.empty()) {
subers.erase(it);
}
}
}
template<typename EventT>
void Pub(const EventT& event)
{
// prevent infinite recursion
static thread_local int depth = 0;
constexpr int MAX_DEPTH = 10;
if (depth >= MAX_DEPTH) {
// log error
return;
}
depth++;
std::vector<std::shared_ptr<EventListenerBase>> validListeners;
{
#ifdef ENABLE_EVENTBUS_MULTI_THREAD
std::lock_guard<std::mutex> lock(mutex);
#endif
// get valid ones and prevent callbacks from modifying the subers map during iteration
// we cant't put validListeners outside, because Pub might be called recursively
std::type_index typeIndex = typeid(EventT);
if (subers.find(typeIndex) != subers.end())
{
auto& list = subers[typeIndex];
for (auto it = list.begin(); it != list.end(); )
{
if (auto lsner = it->lock())
{
validListeners.push_back(lsner);
++it;
}
else
{
it = list.erase(it);
}
}
}
}
for (auto& lsner : validListeners) {
try
{
auto derivedLsner = std::static_pointer_cast<EventListener<EventT>>(lsner);
if (derivedLsner)
{
derivedLsner->callback(event);
}
else
{
// log error
continue;
}
}
catch (...)
{
// log error
continue;
}
}
depth--;
}
};
class EventRegister
{
private:
#ifdef ENABLE_EVENTBUS_MULTI_THREAD
std::atomic<int> id = 0;
#else
int id = 0;
#endif
public:
std::vector<std::shared_ptr<EventListenerBase>> listeners;
template<typename EventT>
int Sub(std::function<void(const EventT&)> callback)
{
auto& bus = EventBus::GetInst();
auto listener = std::make_shared<EventListener<EventT>>(callback);
listener->ID = ++id;
bus.Sub(listener);
listeners.push_back(std::move(listener));
return id;
}
void UnSub(int ID)
{
auto& bus = EventBus::GetInst();
auto it = std::remove_if(listeners.begin(), listeners.end(),
[&](const std::shared_ptr<EventListenerBase>& lsner) {
if (lsner->ID == ID)
{
bus.UnSub(lsner);
return true;
}
return false;
});
listeners.erase(it, listeners.end());
}
void Clear()
{
auto& bus = EventBus::GetInst();
for (auto& lsner : listeners)
{
bus.UnSub(lsner);
}
listeners.clear();
}
};