void Animal::notifyEatingInitiated() {
log.debug(TAG, "notifyEatingInitiated called");
std::lock_guard<std::mutex> lock(m_mutex);
auto observers = m_observers;
for (const auto& observer : observers) {
observer->notifyEatingInitiated();
}
}
我正在学习 C++ 教程,在这里我看到在关键部分中我们正在创建实例变量的副本
m_observers
,然后对其进行迭代。
是否需要创建一个副本
m_observers
临界区不是会锁定当前作用域(这里
m_observers
)之外的变量来进行修改吗?
从线程安全的角度来看这是没有必要的。 有以下几种可能:
m_observers
到处都有 std::mutex
妥善保护,因此简单地循环 m_observers
m_observers
在其他位置没有被 std::mutex
正确保护,在这种情况下,在 auto observers = m_observers;
中调用复制构造函数无论如何都是数据争用无论哪种情况,为了线程安全的目的而复制
m_observers
都是没有意义的。
这样写可能有意义:
auto observers = [this] {
std::lock_guard<std::mutex> lock(m_mutex);
return m_observers;
}(); // immediately invoked lambda expression (IILE)
for (const auto& observer : observers) {
observer->notifyEatingInitiated();
}
如果
notifyEatingInitiated()
需要很长时间,并且复制m_observers
很便宜,那么这将是一个重大改进,因为它限制了在关键部分花费的时间。
您的示例甚至没有尝试获得复制的好处observers
,因此教程的作者更有可能误解了该主题,或者犯了一个错误。
notifyEatingInitiated()
修改了 m_observers
呢?正如评论者@HolyBlackCat 所指出的,
notifyEatingInitiated()
可能会修改m_observers
。
在这种情况下,有必要复制m_observers
以避免在迭代过程中修改容器。
虽然这会令人惊讶且容易出现错误的设计,但有必要像作者在您的示例中所做的那样完全编写代码。
在您的具体情况下,整个代码已损坏。每个线程通过复制一些不相关的观察者来创建并通知它们,而
m_observers
永远不会被通知。
void Animal::notifyEatingInitiated() {
log.debug(TAG, "notifyEatingInitiated called");
std::lock_guard<std::mutex> lock(m_mutex);
auto observers = m_observers; //observers at this point are copy of m_observers not refference to m_observers
for (const auto& observer : observers) {
observer->notifyEatingInitiated();
}
}
这种复制的使用是无关紧要的,并且完全杀死了代码。复制比构建便宜,因此应该使用,因为在适当的地方速度更快。对象的副本绝不是初始对象的别名,它是全新的对象。了解浅复制和深复制。如上所述,线程安全得不到保证。为了线程安全,始终使用保护和公开方法进行包装,例如
safe_notify_all()
,其中 m_mutex
和 m_observers
都是包装器的私有成员。