【问题标题】:What's the best way to synchronize this event implementation同步此事件实现的最佳方法是什么
【发布时间】:2012-01-11 23:53:42
【问题描述】:

这是我实现 C++ 事件的尝试。

class Event{
    typedef std::tr1::function<void( int& )> CallbackFunction;
    std::list< CallbackFunction > m_handlers;

    template<class M>
    void AddHandler(M& thisPtr, void typename (M::*callback)(int&)) 
    {           
        CallbackFunction bound = std::tr1::bind(callback, &thisPtr, _1);
        m_handlers.push_back(bound);
    }

    void operator()(int& eventArg) 
    { 
        iterate over list...
        (*iter)(eventArg);

    }}

这里的问题是线程安全。如果同时调用 AddHandler 和 operator(),事情可能会中断。

同步这个的最佳方式是什么?使用互斥锁可能会降低性能。我想知道在这种情况下 boost::signals 或 C# 事件的幕后会发生什么。

【问题讨论】:

  • 他们每秒被调用的频率是多少?为什么同一个对象需要对所有线程都可用?他们不能都只发送一个线程接收的互斥消息吗?

标签: c++ multithreading events delegates


【解决方案1】:

首先,在您将任何实施可能性视为不够“快速”之前,您需要确定实际的性能要求是什么。您会每秒触发数千次这些事件吗?如果是,您是否真的需要一直将处理程序添加到处理程序容器中。

如果出于某种原因,这两个问题的答案实际上都是“是”,那么您可能需要调查无锁容器。这将意味着构建您自己的容器,而不是能够使用 stl 列表。无锁容器将使用原子内在函数(例如 Windows 中的 InterlockedCompareExchange)来原子地确定列表的末尾是否为 NULL 或其他。然后他们将使用类似的内在函数来实际附加到列表中。如果多个线程同时尝试添加处理程序,则会出现其他复杂情况。

但是,在多核机器和指令重新排序等的世界中,这些方法可能充满危险。我个人使用的事件系统与您描述的不同,我将它与关键部分一起使用(至少在 Windows 中非常有效),并且我没有遇到性能问题。但另一方面,通过事件系统发送的速度不会超过 20Hz 左右。

与任何与性能相关的问题一样,答案总是基于另一个问题的答案;您究竟在哪里需要您的表现?

【讨论】:

  • 此事件是一个通用帮助器,因此如果它能够有效地处理任何使用模式,则它是最佳的。感谢您分享您的经验,这将我推向了critical_section 解决方案。
【解决方案2】:

互斥锁绝对是您要找的。如果每个事件都有自己的互斥体,我不会太担心性能;原因是除非您在处理事件期间添加大量处理程序,否则互斥锁不太可能争用并减慢您的速度。

但是,如果您有多个线程在同一个对象上调用 operator() 方法,则此互斥锁可能会出现问题。但是没有它,你将如何确保以线程安全的方式调用你的回调呢? (我注意到你传入一个整数引用并返回 void,所以我假设这些不是可重入处理程序。)

编辑:您的评论中提出了非常好的问题。老实说,我从来没有考虑过以同步方式使用互斥锁是否有很多开销。所以我把这个小测试放在一起。

#include <stdio.h> #include <pthread.h> #define USE_PTHREAD_MUTEX 1 int main(int argc, char * argv[]) { pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL); long useless_number = 0; long counter; for(counter = 0; counter < 100000000; counter++) { #if USE_PTHREAD_MUTEX pthread_mutex_lock(&mutex); #endif useless_number += rand(); #if USE_PTHREAD_MUTEX pthread_mutex_unlock(&mutex); #endif } printf("%ld\n", useless_number); }

我在我的系统上运行它并获得了以下运行时。

使用 USE_PTHREAD_MUTEX 0,平均运行时间为 1.2 秒。

使用 USE_PTHREAD_MUTEX 1,平均运行时间为 2.8 秒。

因此,要回答您的问题,肯定会有开销。你的旅费可能会改变。此外,如果多个线程竞争对资源的访问,则必然会花费更多时间进行阻塞。此外,在纯同步上下文中,访问共享资源可能会花费更多时间,而不是等待互斥锁锁定/解锁。也就是说,与这些东西相比,互斥逻辑本身的开销可能微不足道。

【讨论】:

  • 没有争用的互斥锁有多贵?说我同步调用AddHandler 100000 次。预计会有什么性能损失?
  • 好问题。我已经通过一个小测试更新了我的答案,因为我也对这种性能影响感到好奇。
  • 在 operator() 中复制回调列表。仅在复制期间获取互斥锁 - 限制锁定的持续时间。由于存在死锁的风险,您也不希望通知循环周围有互斥锁。如果线程争用较低,您可以考虑使用自旋锁 - 比互斥锁快约 4 倍,但如果在高争用情况下锁定时间过长(例如,避免在锁定时分配内存),则会发生灾难。分析是唯一确定的方法。
【解决方案3】:

如果列表真的是你的类,那么由于它的性质,你不需要每次访问它时都锁定。您将锁定一个互斥锁以发布到列表的末尾,并且当您认为您可能已到达末尾时您也会锁定。

你应该计算类中处理程序的数量,当你即将开始迭代时,你可以愉快地迭代而无需锁定,直到达到这个数量。

如果要删除处理程序,那么您就会遇到更多的线程争用问题。

【讨论】:

  • 这是一个很好的观察。我可以使用联锁...来计算。但不幸的是,我可能会添加一个“删除”功能,因此需要更好的解决方案。有什么想法吗?
  • 如果您收到“取消订阅”,您可以用标志标记记录,而不是将其从列表中移出。事件处理线程检查标志并可以从列表中删除未订阅的项目,如果它是最后一个列表项,您只需要锁定,因为只有列表的末尾有争用。
猜你喜欢
  • 2011-02-13
  • 2021-06-06
  • 2016-12-24
  • 1970-01-01
  • 2018-02-10
  • 2020-09-27
  • 2020-01-15
  • 2010-09-14
  • 2010-09-09
相关资源
最近更新 更多