C++ 事件系统 - 基于堆的事件触发

Posted

技术标签:

【中文标题】C++ 事件系统 - 基于堆的事件触发【英文标题】:C++ Event System - Heap based Event fire 【发布时间】:2021-11-26 22:32:42 【问题描述】:

我已经开始了我的游戏引擎之旅,我计划让它成为多线程的。考虑到这一点和堆分配成本,我编写了一个 EventPool 类,负责缓存事件分配,从而减少堆分配,但它增加了搜索先前分配的相同类型的“空闲”事件指针的额外成本。理想情况下,您只想进行堆栈分配,但考虑到每个 EventType 都需要向下转换,我不确定这是否可行。

每个事件触发并删除每个帧或保留 EventPool 并从中搜索是否更好?

事件触发:

template<typename EventType>
static void Publish(const EventType& e)

    const auto& handlerIt = m_Subscribers.find(typeid(EventType));

    if (handlerIt == m_Subscribers.end())
    
        return;
    

    auto ev = EventPool::Allocate<EventType>(e);

    for (auto& handler : handlerIt->second)
    
        if (!ev->IsHandled())
        
            ev->m_Handled = handler(ev);
        
    

    ev->m_Handled = true;

如果 EventPool 方法更好,我该如何优化它?

EventPool 分配器:

template<class EventType>
static EventType* Allocate(const EventType& e)

    const auto& poolIt = m_EventPool.find(typeid(EventType));

    EventType* newEvent;
    if (poolIt == m_EventPool.end())
    
        newEvent = new EventType;
        memcpy(newEvent, &e, sizeof(EventType));
        m_EventPool[typeid(EventType)].push_back(newEvent);
        return newEvent;
    
    else
    
        for (Event* ev : poolIt->second)
        
            if (ev->IsHandled())
            
                newEvent = static_cast<EventType*>(ev);
                memcpy(newEvent, &e, sizeof(EventType));
                return newEvent;
            
        

        newEvent = new EventType;
        memcpy(newEvent, &e, sizeof(EventType));
        poolIt->second.push_back(newEvent);
        return newEvent;
    

【问题讨论】:

do heap allocation per event fire and delete per frame 让我觉得你的所有事件都没有比游戏帧长(即使在游戏结束时未处理)。我说的对吗? 是的,至少在单线程上不行。在多线程系统中,事件的句柄可能会延迟 我错过了这个“(即使在它结束时未处理)”。答案是否定的,在处理之前不会被删除。 【参考方案1】:

根据

template&lt;typename EventType&gt; static void Publish(const EventType&amp; e)

,编译器提前知道你的事件的所有类型(因为它必须为每个事件实例化Publish模板函数)。您可以使用这个事实来完全避免运行时类型分派。例如:

template<typename E> using Subscriber = bool (*)(E const&); // or whatever it is

template<typename... Es> // all of the events which may be worked with go here
class EventPool 
private:
  std::tuple<std::vector<Es>...> storages; // a storage per event type
  std::tuple<std::vector<Subscriber<Es>>...> subscribers; // a list of subscribers per event type
  // or whatever you prefer over vectors

public:
  template<typename E> void publish(E const& event) 
    get<E>(storages).push_back(event); // or whatever your (re)allocation policy is
    for (auto& handler: get<Subscriber<E>>(subscribers))
      if (event.handled = handler(event))
        break;
  
;

现在所有 RTTI find(typeid(E)) 都消失了 - 相反,您可以在编译时通过 std::get 在所有可能变体的元组上为您的事件类型选择适当的存储。此外,你不需要不安全的向下转型

newEvent = static_cast&lt;EventType*&gt;(ev)

因为只要不擦除类型就不需要恢复类型,就像在这种方法中一样。

谈到堆分配 - 您的事件不需要一个一个newed,这样可以为您节省大量分配。相反,为每种事件类型提供了一个存储(在我的例子中是动态数组 (std::vector)),因此事件可以就地存储。

【讨论】:

【参考方案2】:

数据结构

听起来您需要一个“池分配器”,这是一种特殊的分配器类型。我们还有“堆栈分配器”、“线性分配器”、“双端堆栈分配器”、“堆分配器”等。

这篇文章很好地概述了所涉及的内存操作: http://dmitrysoshnikov.com/compilers/writing-a-pool-allocator/

但是,这样的分配器在事件类型时起作用,即。 EventType,具有可预测且未修改的大小(以字节为单位)。池分配器的实现依赖于一个数组,其中每个元素的内存是 reinterpret_cast'ed 到 EvenType* 到下一个空闲项目。因此,它使用隐式空闲列表但没有单独的列表结构,因为列表包含在数据中。这是一个非常简单的代码来展示它是如何工作的:

// This static assert is very important!
static_assert(sizeof(EventType) >= sizeof(void*));

EvenType pool[5];
EventType* free_ptr = &pool[0];

for (int i = 0; i < 4; i++)

    auto ptr = reinterpret_cast<EventType*>(&pool[i]);
    *ptr = &pool[i+1];

auto ptr = reinterpret_cast<EventType*>(&pool[4]);
*ptr = nullptr;

// now allocate
EventType* new_ptr = nullptr;
if (free_ptr == nullptr)

    // failed to allocate!

new_ptr = free_ptr;
free_ptr = reinterpret_cast<EventType*>(*free_ptr);

// Now, free_ptr should point to &pool[1]

这是一种关于未来发展的蓝图。这种代码很容易出错(尽管我已经做了很多,但我可能会犯一些错误),所以我强烈鼓励进行彻底的单元测试。

好处

听起来堆分配是您关心的问题,在运行时应该始终受到限制,尤其是在游戏引擎消息处理等时间关键型系统中。使用堆分配器,您可以预先分配一大块内存并在整个程序生命周期中使用该内存。因此,您需要提前知道在任何给定时刻,一次即时消息的最大数量。如果超过该数字,则消息将被丢弃,并且应将错误消息输出到记录器。

【讨论】:

以上是关于C++ 事件系统 - 基于堆的事件触发的主要内容,如果未能解决你的问题,请参考以下文章

在 C++ 中触发 COM 事件 - 同步还是异步?

在 C++ 中创建/打开事件并检查它们是不是被触发

在 C++ 中触发事件并在 C# 中处理它们

当控件即将失去焦点时,是不是存在从 C++ 程序触发的事件?

Android事件处理

从 C++ DLL 在 C# 中触发事件