[英]How to handle thread-safe callback registration and execution in C++?
例如,我有一個EventGenerator
類,它為所有注冊的事件處理程序調用IEventHandler::onEvent
:
class IEventHandler {
public: virtual void onEvent(...) = 0;
};
class EventGenerator {
private:
std::vector<IEventHandler*> _handlers;
std::mutex _mutex; // [1]
public:
void AddHandler(IEventHandler* handler) {
std::lock_guard<std::mutex> lck(_mutex); // [2]
_handlers.push_back(handler);
}
void RemoveHanler(IEventHandler* handler) {
std::lock_guard<std::mutex> lck(_mutex); // [3]
// remove from "_handlers"
}
private:
void threadMainTask() {
while(true) {
// Do some work ...
// Post event to all registered handlers
{
std::lock_guard<std::mutex> lck(_mutex); // [4]
for(auto& h : _handlers) { h->onEvent(...); )
}
// Do some work ...
}
}
代碼應該以下列方式是線程安全的:
EventGenerator::threadMainTask
EventGenerator::AddHandler
和EventGenerator::RemoveHandler
API。為了支持這一點,我有以下同步(請參閱代碼中的注釋):
[1]
是保護向量_handlers
免受多線程訪問的互斥鎖。[2]
和[3]
保護同時添加或刪除處理程序。[4]
阻止在主線程發布事件時更改向量。 這段代碼一直工作到...如果由於某種原因,在IEventHandler::onEvent(...)
的執行過程中,代碼試圖調用EventManager::RemoveHandler
或EventManager::AddHandler
。 結果是運行時異常。
以線程安全的方式處理事件處理程序注冊和執行事件處理程序回調的最佳方法是什么?
>> 更新 <<
因此,根據輸入,我已更新為以下設計:
class IEventHandler {
public: virtual void onEvent(...) = 0;
};
class EventDelegate {
private:
IEventHandler* _handler;
std::atomic<bool> _cancelled;
public:
EventDelegate(IEventHandler* h) : _handler(h), _cancelled(false) {};
void Cancel() { _cancelled = true; }
void Invoke(...) { if (!_cancelled) _handler->onEvent(...); }
}
class EventGenerator {
private:
std::vector<std::shared_ptr<EventDelegate>> _handlers;
std::mutex _mutex;
public:
void AddHandler(std::shared_ptr<EventDelegate> handler) {
std::lock_guard<std::mutex> lck(_mutex);
_handlers.push_back(handler);
}
void RemoveHanler(std::shared_ptr<EventDelegate> handler) {
std::lock_guard<std::mutex> lck(_mutex);
// remove from "_handlers"
}
private:
void threadMainTask() {
while(true) {
// Do some work ...
std::vector<std::shared_ptr<EventDelegate>> handlers_copy;
{
std::lock_guard<std::mutex> lck(_mutex);
handlers_copy = _handlers;
}
for(auto& h : handlers_copy) { h->Invoke(...); )
// Do some work ...
}
}
如您所見,還有一個額外的類EventDelegate
有兩個目的:
在threadMainTask
,我使用std::vector<std::shared_ptr<EventDelegate>>
的本地副本,並且在調用回調之前釋放鎖。 這種方法解決了在IEventHandler::onEvent(...)
期間IEventHandler::onEvent(...)
EventGenerator::{AddHandler,RemoveHanler}
的問題。
對新設計有什么想法嗎?
在 shared_ptr 的原子交換上實現的寫時復制向量(假設回調注冊發生的頻率遠低於回調通知的事件):
using callback_t = std::shared_ptr<std::function<void(event_t const&)> >;
using callbacks_t = std::shared_ptr<std::vector<callback_t> >;
callbacks_t callbacks_;
mutex_t mutex_; // a mutex of your choice
void register(callback_t cb)
{
// the mutex is to serialize concurrent callbacks registrations
// this is not always necessary, as depending on the application
// architecture, single writer may be enforced by design
scoped_lock lock(mutex_);
auto callbacks = atomic_load(&callbacks_);
auto new_callbacks = std::make_shared< std::vector<callback_t> >();
new_callbacks->reserve(callbacks->size() + 1);
*new_callbacks = callbacks;
new_callbacks->push_back(std::move(cb));
atomic_store(&callbacks_, new_callbacks);
}
void invoke(event_t const& evt)
{
auto callbacks = atomic_load(&callbacks_);
// many people wrap each callback invocation into a try-catch
// and de-register on exception
for(auto& cb: *callbacks) (*cb)(evt);
}
特別是在取消注冊時執行回調時的異步行為主題,這里最好的方法是記住關注點分離原則。
回調在執行之前不應該死掉。 這是通過另一個稱為“額外間接級別”的經典技巧來實現的。 也就是說,不是注冊用戶提供的回調,而是將其包裝為如下所示的內容,並且除了更新向量之外,回調注銷將調用回調包裝器上定義的discharge()
方法,甚至會通知取消注冊的調用者回調執行是否成功完成的方法。
template <class CB> struct cb_wrapper
{
mutable std::atomic<bool> done_;
CB cb_;
cb_wrapper(CB&& cb): cb(std::move(cb_)) {}
bool discharge()
{
bool not_done = false;
return done_.compare_exchange_strong(not_done, true);
}
void operator()(event_t const&)
{
if (discharge())
{
cb();
}
}
};
我在這里看不到正確的東西。 從您的更新中,我可以看到一個問題:您沒有將調用方法與回調刪除同步。 有一個原子,但這還不夠。 示例:就在這行代碼之后:
if (!_cancelled)
另一個線程調用 remove 方法。 可能發生的情況是onEvent()
無論如何都會被調用,即使刪除的方法已經從列表中刪除了回調並返回了結果,也沒有什么可以保持同步這個執行流程。 @bobah 的回答也有同樣的問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.