2012-01-11 119 views
1

这是我尝试实现一个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); 

    }} 

这里的麻烦是线程安全。如果AddHandleroperator()被调用的同时事情可能会中断。

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

+1

他们每秒打电话多久?为什么这个相同的对象需要对所有线程都可用?难道他们不能只发送一个线程接收到的互斥信息吗? – 2012-01-12 00:02:34

回答

1

互斥量绝对是你要找的。如果每个事件都有自己的互斥量,我不会担心很多性能问题。原因在于,除非在处理事件期间添加了大量的处理程序,否则互斥体不太可能出现争用并使您放慢速度。

但是,如果你有多个线程在相同的对象上调用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秒。

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

+0

互斥体没有争用有多昂贵?说我同步调用'AddHandler'100000次。这里期望的性能损害是什么? – Leo 2012-01-12 00:25:22

+0

伟大的问题。我已经通过一些测试更新了我的答案,因为我也对这种性能影响感到好奇。 – Tom 2012-01-12 01:34:18

+2

在operator()中复制回调列表。仅在复制期间获取互斥锁 - 限制锁的持续时间。由于死锁的风险,您也不希望在通知循环周围出现互斥锁。如果线程争用比较低,您可以考虑使用自旋锁 - 比互斥锁快4倍,但如果锁在高争用中保持时间过长(例如,锁定时避免内存分配),则会导致灾难。分析是确定知道的唯一方法。 – mcmcc 2012-01-12 03:59:06

2

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

如果出于某种原因,对这两个问题的答案实际上是'是',那么您可能需要调查无锁容器。这将意味着构建自己的容器,而不是使用stl列表。无锁容器将使用原子内在函数(例如Windows中的InterlockedCompareExchange)来自动确定列表的末尾是否为NULL。然后他们会使用类似的内在属性来实际追加到列表中。如果多个线程同时尝试添加处理程序,将会发生其他复杂情况。然而,在多核机器和指令重新排序的世界中,这些方法可能充满危险。我个人使用的事件系统与您所描述的不一样,我将它与Critical Sections一起使用(至少在Windows中非常高效),而且我没有遇到性能问题。但另一方面,通过事件系统的任何事情都不会超过20Hz左右。

与任何与表现有关的问题,答案总是基于对另一个问题的答案;具体到哪里你需要你的表现?

+0

这个事件是一个通用的帮助器,因此如果它有效地处理任何使用模式,它是最佳的。感谢您分享您的经验,这将我推向critical_section解决方案。 – Leo 2012-01-12 00:34:54

1

如果列表真的是你的班级,那么由于它的性质,每次访问时都不需要锁定。您将锁定一个互斥锁并将其张贴到列表的末尾,并且当您认为您可能已达到结尾时您还将锁定。

您应该保持对类中处理程序数量的计数,并且当您要开始迭代时,您可以快速迭代而不锁定,直到达到此数字。

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

+0

这是一个很棒的观察。我可以使用联锁...来计数。但不幸的是,我可能会添加一个“删除”功能,因此需要更好的解决方案。有什么想法吗? – Leo 2012-01-12 00:18:39

+0

如果您收到“取消订阅”,则用标记标记该记录,而不是将其从列表中移出。事件处理程序线程检查标志并可以从列表中删除未订阅的项目,并且只需要锁定它是否为最后一个列表项目,因为只有列表的末尾存在争用。 – CashCow 2012-01-12 10:21:29

相关问题