事件系统设计(C++)

具体调试代码参考github:https://github.com/hggzhang/CppTest/tree/master

概述

在程序设计中,我们希望关联程度低的对象之间的联系是"松耦合"的,也即减少直接依赖。一般的做法是使用消息机制进行信息的传递和响应,其中事件系统是其一种常规手段之一,下面我们尝试使用C++实现一个事件系统。

观察者模式

假如你想买一份报纸:

  1. 不停的去邮局问今天的报纸到了嘛?到了嘛?到了嘛?... (轮询模式)
  2. 去邮局登记要买今天的报纸,之后就回家;邮局报纸到了之后,邮局按照登记名单送报纸。(观察者模式)

对比这两种方式,相对你而言是不是方法2更加高效?你不必一直在邮局等着一直问报纸到了没,登记信息回家看电视等着就好了。所谓观察者模式基本就是这种思想,其核心要素有:

  1. 订阅消息:对某个感兴趣的消息A进行登记。
  2. 发布消息:"相关单位"发布消息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();
	}
};