2012-01-11 117 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

相關問題