假设添加了_mutex 以保护其他字段不被同时访问,您将需要在分配期间锁定互斥锁,除非您可以保证多个线程不会访问分配的任何一侧表达式:
x = y;
如果任何其他线程同时读取或写入x,则您将进行一场没有锁定的比赛。
如果任何其他线程同时向y 写入数据,则您将进行一场没有锁定的比赛。
如果确实需要加锁,也不是两边加锁那么简单:
MyStruct& operator=(const MyStruct& o)
{
if (this != &o)
{
// WRONG! DO NOT DO THIS!!!
std::lock_guard<std::mutex> lhs_lk(_mutex);
std::lock_guard<std::mutex> rhs_lk(o._mutex);
field1 = o.field1;
field2 = o.field2;
}
return *this;
}
你不应该这样做的原因是想象线程 A 这样做:
x = y;
同时线程 B 这样做:
y = x;
现在你有可能出现死锁。想象一下线程 A 锁定 x._mutex,然后线程 B 锁定 y._mutex。现在线程 A 和 B 都将阻止尝试锁定它们的 o._mutex,并且两个线程都不会成功,因为它正在等待另一个线程释放它。
赋值运算符的正确表述如下:
MyStruct& operator=(const MyStruct& o)
{
if (this != &o)
{
std::lock(_mutex, o._mutex);
std::lock_guard<std::mutex> lhs_lk(_mutex, std::adopt_lock);
std::lock_guard<std::mutex> rhs_lk(o._mutex, std::adopt_lock);
field1 = o.field1;
field2 = o.field2;
}
return *this;
}
std::lock(m1, m2, ...) 的工作是以某种神奇的方式锁定所有互斥锁,而 不会 死锁。如果您想了解更多细节,请阅读Dining Philosophers Rebooted。
现在_mutex 和o._mutex 已锁定,您只需通过拥有lock_guards adopt 对它们的互斥锁的所有权来确保它们的解锁异常安全。也就是说,它们将不再尝试在构造时锁定它们的互斥体,但它们仍会在破坏时解锁它们。 lock_guard 构造函数本身什么也不抛出,所以这都是异常安全的。
哦,您还必须将 _mutex 存储为 mutable 数据成员,否则您将无法在 rhs 上锁定和解锁它。
在 C++14 中,如果您想尝试,可以使用潜在的优化:您可以“写锁定”this->_mutex 和“读锁定”o._mutex。这将允许多个线程同时为 from 分配一个公共 rhs,如果没有线程将 分配给该 rhs。为此,您需要让MyStruct 存储std::shared_timed_mutex 而不是std::mutex:
#include <mutex>
#include <shared_mutex>
struct MyStruct
{
using MutexType = std::shared_timed_mutex;
using ReadLock = std::shared_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;
mutable MutexType _mutex;
int field1;
int field2;
MyStruct& operator=(const MyStruct& o)
{
if (this != &o)
{
WriteLock lhs_lk(_mutex, std::defer_lock);
ReadLock rhs_lk(o._mutex, std::defer_lock);
std::lock(lhs_lk, rhs_lk);
field1 = o.field1;
field2 = o.field2;
}
return *this;
}
};
这与以前类似,只是我们需要更改互斥体的类型,现在 lhs 锁定 unique_lock(它写锁定 lhs 互斥体)和 rhs 锁定 shared_lock(它读取-锁定 rhs 互斥体)。这里我们还使用std::defer_lock 来构造锁,但要告诉锁互斥体尚未锁定,并且在构造时不要锁定。然后我们的老朋友std::lock(m1, m2) 用来告诉两个锁同时加锁而不会死锁。是的,std::lock 适用于互斥锁和锁类型。任何具有成员 lock()、try_lock() 和 unlock() 的内容都将与 std::lock(m1, m2, ...) 一起使用。
请注意,C++14 技术并不是绝对的优化。您将不得不测量以确认或否认它是。对于像MyStruct 这样简单的东西,它可能不是优化,除了一些特殊的使用模式。带有std::mutex 的 C++11 技术仍然是工具箱中的一个有价值的工具,即使在 C++14 中也是如此。
为了便于在mutex 和shared_timed_mutex 之间切换回来和第四次,这个最新的示例使用了类型别名,可以轻松更改。只需更改两行即可切换回mutex:
using MutexType = std::shared_timed_mutex;
using ReadLock = std::共享unique_lock<MutexType>;
using WriteLock = std::unique_lock<MutexType>;