在 c++ 中使用 RAII 进行回调注册

Using RAII for callback registration in c++

本文关键字:回调 注册 RAII c++      更新时间:2023-10-16

我正在使用一些API来获取通知。像这样:

NOTIF_HANDLE register_for_notif(CALLBACK func, void* context_for_callback);
void unregister_for_notif(NOTIF_HANDLE notif_to_delete);

我想把它包装在一些体面的 RAII 类中,该类将在收到通知时设置一个事件。我的问题是如何同步它。我写了这样的东西:

class NotifClass
{
public:
NotifClass(std::shared_ptr<MyEvent> event):
_event(event),
_notif_handle(register_for_notif(my_notif_callback, (void*)this))
// initialize some other stuff
{
// Initialize some more stuff
}
~NotifClass()
{
unregister_for_notif(_notif_handle);
}
void my_notif_callback(void* context)
{
((NotifClass*)context)->_event->set_event();
}
private:
std::shared_ptr<MyEvent> _event;
NOTIF_HANDLE _notif_handle;
};

但是我担心在构造\销毁期间调用回调(也许在这个特定示例中,shared_ptr会很好,但对于其他构造类,它可能会有所不同)。

我会再说一遍 - 我不想要这个非常具体的类的非常具体的解决方案,而是在传递回调时为 RAII 提供更通用的解决方案。

你对同步的担忧有点错位。

为了总结您的问题,您有一些库,您可以使用它注册回调函数,以及(通过 void* 指针或类似)一些资源,该函数通过register()函数对其进行操作。该库还提供unregister()函数。

在您的代码中,您既不能也不应该尝试防止库可以在通过unregister()函数取消注册后或取消注册时调用您的回调函数的可能性:库有责任确保回调在取消注册时或取消注册后无法触发。图书馆应该担心同步、互斥体和其他的 gubbins,而不是你。

代码的两个职责是:

  • 确保在注册回调之前构造回调作用的资源,并且
  • 请确保在销毁回调所针对的资源之前取消注册回调。

构造与破坏的这种反顺序正是C++对其成员变量所做的,也是编译器在以"错误"顺序初始化它们时警告您的原因。

就您的示例而言,您需要确保 1)register_for_notif()在初始化共享指针后调用,2) 在 std::shared_ptr(或其他任何内容)被销毁之前调用 2)unregister_for_notif()

后者的关键是理解析构函数中的销毁顺序。有关回顾,请查看以下 cppreference.com 页面的"销毁序列"部分。

  • 首先,执行析构函数的主体;
  • 然后,编译器按与声明相反的顺序调用类的所有非静态非变量成员的析构函数。

因此,您的示例代码是"安全的"(或尽可能安全),因为在销毁成员变量std::shared_ptr<MyEvent> _event之前,在析构函数主体中调用unregister_for_notif()

另一种(在某种意义上更明确地遵循 RAII)方法是通过将通知句柄拆分为自己的类来将通知句柄与回调函数运行的资源分开。 例如:

class NotifHandle {
public:
NotifHandle(void (*callback_fn)(void *), void * context)
: _handle(register_for_notif(callback_fn, context)) {}
~NotifHandle() { unregister_for_notif(_handle); }
private:
NOTIF_HANDLE _handle;
};
class NotifClass {
public:
NotifClass(std::shared_ptr<MyEvent> event)
: _event(event),
_handle(my_notif_callback, (void*)this) {}
~NotifClass() {}
static void my_notif_callback(void* context) {
((NotifClass*)context)->_event->set_event();
}
private:
std::shared_ptr<MyEvent> _event;
NotifHandle _handle;
};

重要的是成员变量声明顺序:NotifHandle _handle是在资源std::shared_ptr<MyEvent> _event之后声明的,因此保证在销毁资源之前取消注册通知。

您可以通过对静态容器的线程安全访问来执行此操作,该静态容器包含指向实时实例的指针。RAII 类构造函数将this添加到容器中,析构函数将其删除。回调函数根据容器检查上下文,如果上下文不存在,则返回。它将看起来像这样(未经测试):

class NotifyClass {
public:
NotifyClass(const std::shared_ptr<MyEvent>& event)
: event_(event) {
{
// Add to thread-safe collection of instances.
std::lock_guard<std::mutex> lock(mutex_);
instances_.insert(this);
}
// Register the callback at the end of the constructor to
// ensure initialization is complete.
handle_ = register_for_notif(&callback, this);
}
~NotifyClass() {
unregister_for_notif(handle_);
{
// Remove from thread-safe collection of instances.
std::lock_guard<std::mutex> lock(mutex_);
instances_.erase(this);
}
// Guaranteed not to be called from this point so
// further destruction is safe.
}
static void callback(void *context) {
std::shared_ptr<MyEvent> event;
{
// Ignore if the instance does not exist.
std::lock_guard<std::mutex> lock(mutex_);
if (instances_.count(context) == 0)
return;
NotifyClass *instance = static_cast<NotifyClass*>(context);
event = instance->event_;
}
event->set_event();
}
// Rule of Three. Implement if desired.
NotifyClass(const NotifyClass&) = delete;
NotifyClass& operator=(const NotifyClass&) = delete;
private:
// Synchronized associative container of instances.
static std::mutex mutex_;
static std::unordered_set<void*> instances_;
const std::shared_ptr<MyEvent> event_;
NOTIF_HANDLE handle_;
};

请注意,回调会在使用共享指针之前递增共享指针并释放容器上的锁。这可以防止在触发MyEvent可以同步创建或销毁NotifyClass实例时出现潜在的死锁。


从技术上讲,上述操作可能会由于地址重用而失败。也就是说,如果一个NotifyClass实例被销毁,并且立即在完全相同的内存地址上创建一个新实例,那么可以想象,针对旧实例的 API 回调可能会传递到新实例。对于某些用法,甚至大多数用法,这无关紧要。如果它确实很重要,则必须使静态容器键全局唯一。这可以通过将集合替换为映射并传递映射键而不是指向 API 的指针来完成,例如:

class NotifyClass {
public:
NotifyClass(const std::shared_ptr<MyEvent>& event)
: event_(event) {
{
// Add to thread-safe collection of instances.
std::lock_guard<std::mutex> lock(mutex_);
key_ = nextKey++;
instances_[key_] = this;
}
// Register the callback at the end of the constructor to
// ensure initialization is complete.
handle_ = register_for_notif(&callback, reinterpret_cast<void *>(key_));
}
~NotifyClass() {
unregister_for_notif(handle_);
{
// Remove from thread-safe collection of instances.
std::lock_guard<std::mutex> lock(mutex_);
instances_.erase(key_);
}
// Guaranteed not to be called from this point so
// further destruction is safe.
}
static void callback(void *context) {
// Ignore if the instance does not exist.
std::shared_ptr<MyEvent> event;
{
std::lock_guard<std::mutex> lock(mutex_);
uintptr_t key = reinterpret_cast<uintptr_t>(context);
auto i = instances_.find(key);
if (i == instances_.end())
return;
NotifyClass *instance = i->second;
event = instance->event_;
}
event->set_event();
}
// Rule of Three. Implement if desired.
NotifyClass(const NotifyClass&) = delete;
NotifyClass& operator=(const NotifyClass&) = delete;
private:
// Synchronized associative container of instances.
static std::mutex mutex_;
static uintptr_t nextKey_;
static std::unordered_map<unsigned long, NotifyClass*> instances_;
const std::shared_ptr<MyEvent> event_;
NOTIF_HANDLE handle_;
uintptr_t key_;
};

RAII 回调有两种常见的通用解决方案。 一个是对象shared_ptr的公共接口。 另一个是std::function.

使用通用接口允许一个smart_ptr控制对象所有回调的生存期。 这类似于观察者模式。

class Observer
{
public:
virtual ~Observer() {}
virtual void Callback1() = 0;
virtual void Callback2() = 0;
};
class MyEvent 
{
public:
void SignalCallback1() 
{
const auto lock = m_spListener.lock();
if (lock) lock->Callback1();
}
void SignalCallback2() 
{
const auto lock = m_spListener.lock();
if (lock) lock->Callback2();
}
void RegisterCallbacks(std::shared_ptr<Observer> spListener) 
{
m_spListener = spListener;
}
private:
std::weak_ptr<Observer> m_spListener;
};
class NotifClass : public Observer
{
public:
void Callback1() { std::cout << "NotifClass 1" << std::endl; }
void Callback2() { std::cout << "NotifClass 2" << std::endl; }
};

示例使用。

MyEvent source;
{
auto notif = std::make_shared<NotifClass>();
source.RegisterCallbacks(notif);
source.SignalCallback1(); // Prints NotifClass 1
}
source.SignalCallback2(); // Doesn't print NotifClass 2

如果使用 C 样式的成员指针,则必须担心对象的地址和成员回调。std::function可以用lambda很好地封装这两件事。 这允许您单独管理每个回调的生存期。

class MyEvent 
{
public:
void SignalCallback() 
{
const auto lock = m_spListener.lock();
if (lock) (*lock)();
}
void RegisterCallback(std::shared_ptr<std::function<void(void)>> spListener) 
{
m_spListener = spListener;
}
private:
std::weak_ptr<std::function<void(void)>> m_spListener;
};
class NotifClass
{
public:
void Callback() { std::cout << "NotifClass 1" << std::endl; }
};

示例使用。

MyEvent source;
// This doesn't need to be a smart pointer.
auto notif = std::make_shared<NotifClass>();
{
auto callback = std::make_shared<std::function<void(void)>>(
[notif]()
{
notif->Callback();
});
notif = nullptr; // note the callback already captured notif and will keep it alive
source.RegisterCallback(callback);
source.SignalCallback(); // Prints NotifClass 1
}
source.SignalCallback(); // Doesn't print NotifClass 1

AFAICT,您担心my_notif_callback可以与析构函数并行调用,context可以是悬而未决的指针。这是一个合理的担忧,我认为你不能用简单的锁定机制来解决它。

相反,您可能需要使用共享指针和弱指针的组合来避免此类悬空指针。例如,要解决您的问题,您可以将事件存储在widget(shared_ptr中),然后您可以创建对widgetweak_ptr并将其作为上下文传递给register_for_notif

换句话说,NotifClassWidget具有share_ptr,上下文是对Widgetweak_ptr。如果无法锁定weak_ptr则类已被销毁:

class NotifClass
{
public:
NotifClass(const std::shared_ptr<MyEvent>& event):
_widget(std::make_shared<Widget>(event)),
_notif_handle(register_for_notif(my_notif_callback, (void*)new std::weak_ptr<Widget>(_widget)))
// initialize some other stuff
{
// Initialize some more stuff
}
~NotifClass()
{
unregister_for_notif(_notif_handle);
}
static void my_notif_callback(void* context)
{
auto ptr = ((std::weak_ptr<Widget>*)context)->lock();
// If destructed, do not set the event.
if (!ptr) {
return;
}
ptr->_event->set_event();
}
private:
struct Widget {
Widget(const std::shared_ptr<MyEvent>& event)
: _event(event) {}
std::shared_ptr<MyEvent> _event;
};
std::shared_ptr<Widget> _widget;
NOTIF_HANDLE _notif_handle;
};

请注意,要添加到NotifClass中的任何功能实际上都应该进入Widget.如果没有此类额外功能,可以跳过Widget间接寻址并使用weak_ptrevent作为上下文:

class NotifClass
{
public:
NotifClass(const std::shared_ptr<MyEvent>& event):
_event(event),
_notif_handle(register_for_notif(my_notif_callback, (void*)new std::weak_ptr<MyEvent>(event)))
// initialize some other stuff
{
// Initialize some more stuff
}
~NotifClass()
{
unregister_for_notif(_notif_handle);
}
static void my_notif_callback(void* context)
{
auto ptr = ((std::weak_ptr<MyEvent>*)context)->lock();
// If destructed, do not set the event.
if (!ptr) {
return;
}
ptr->set_event();
}
private:
std::shared_ptr<MyEvent> _event;
NOTIF_HANDLE _notif_handle;
};

版主警告:为了请求我删除此帖子,只需编辑它!

在注册回调对象之前,请确保回调对象已完全构造。意味着,使回调对象成为单独的类,使注册/取消注册包装器成为单独的类。 然后,可以将这两个类链接到成员或基类关系中。

结构 A { CCallBackObject m_sCallback; 注册m_sRegistration; A(无效)  :m_sCallback(),  m_sRegistration(&m_sCallback)  {  } };

作为另一个好处,您可以重复使用注册/取消注册包装器...

如果回调可能发生在另一个线程中,我会重新设计这个软件以避免这种情况。 例如,可以使主线程的关闭(例如此对象的破坏)等到所有工作线程都关闭/完成。