这是我尝试实现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#事件的幕后会发生什么。
答案 0 :(得分:2)
首先,在您将任何实施可能性视为“不够快”之前,您需要确定实际的性能要求。您是否会每秒触发这些事件数千次?如果你是,那么你是否真的需要一直在处理程序容器中添加处理程序。
如果这两个问题的答案出于某种原因实际上是“是”,那么您可能需要调查无锁容器。这意味着构建自己的容器而不是能够使用stl列表。无锁容器将使用原子内在函数(例如,在Windows中的InterlockedCompareExchange)以原子方式确定列表的末尾是否为NULL或其他。然后,他们将使用类似的内在实际附加到列表中。如果多个线程同时尝试添加处理程序,则会发生其他复杂情况。
然而,在多核机器和指令重新订购等等的世界中,这些方法可能充满危险。我个人使用的事件系统与你描述的不同,我将它用于关键部分(至少在Windows中非常有效),而且我没有遇到性能问题。但另一方面,通过事件系统发送的任何信息都不会超过20Hz左右。
与任何与绩效相关的问题一样,答案总是基于另一个问题的答案;你究竟在哪里需要你的表现?
答案 1 :(得分: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秒。
因此,要回答你的问题,肯定有开销。你的旅费可能会改变。此外,如果多个线程竞争访问资源,则必须花费更多时间来阻止。此外,在纯粹的同步上下文中,与等待互斥锁进行锁定/解锁相比,访问共享资源的时间可能更长。也就是说,与这些事情相比,互斥逻辑本身的开销可能微不足道。
答案 2 :(得分:1)
如果列表确实是您的类,那么由于它的性质,您不需要在每次访问它时锁定。您将锁定互斥锁以发布到列表的末尾,并且当您认为可能已到达结束时也会锁定。
您应该保留班级中处理程序数量的计数,当您即将开始迭代时,您可以愉快地迭代而不会锁定,直到您达到此数字。
如果要删除处理程序,那么您会遇到更多的线程争用问题。