在 C++ 中添加互斥锁后结构中的赋值运算符

Assignment operator in struct after adding mutex in C++

我有一个结构类型:

struct MyStruct {
    int field1;
    int field2;
}

然后有必要向它添加一个互斥锁以使其在线程之间共享:

struct MyStruct {
    std::mutex _mutex;

    int field1;
    int field2;
}

但随后编译器 (clang) 在我将一个现有结构分配给 MyStruct 类型变量的行中给我这些消息,例如 MyStruct mystruct = p_MyStructMap->at(clientId);:

(1) 错误:'MyStruct' 类型的对象无法赋值,因为其复制赋值运算符被隐式删除

(2) 注意:'MyStruct' 的复制赋值运算符被隐式删除,因为字段“_mutex”具有已删除的复制赋值运算符

std::mutex _mutex
           ^     

(3) /usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/mutex:129:12 : 注意:函数已在此处明确标记为已删除

mutex& operator=(const mutex&) = delete;
       ^

求助:我如何重写结构或者可能是程序逻辑为此结构使用互斥体?

std::mutex variables/members 无法赋值,因此,你得到错误。

尝试这样的事情:

struct MyStruct {
    std::mutex _mutex;

    int field1;
    int field2;

   MyStruct &operator=(const MyStruct &o) {
      field1=o.field1;
      field2=o.field2;
      return *this;
   }
};

但是,这不会在分配期间锁定互斥量。根据上下文,您可能需要在赋值运算符或调用函数中添加锁定。

假设添加了 _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.

现在 _mutexo._mutex 已锁定,您只需要通过拥有 lock_guards adopt 所有权来确保他们的解锁异常安全他们的互斥量。也就是说,他们将不再尝试在构造时锁定互斥体,但在销毁时仍会解锁它们。 lock_guard 构造函数本身不抛出任何东西,所以这都是异常安全的。

哦,您还必须将 _mutex 存储为 mutable 数据成员,否则您将无法在 rhs 上锁定和解锁它。

在 C++14 中,如果您想尝试,可以使用潜在的优化:您可以 "write-lock" this->_mutex 和 "read-lock" 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 中也是如此。

为了便于在 mutexshared_timed_mutex 之间来回切换,这个最新示例使用了类型别名,可以轻松更改。只需更改两行即可切换回 mutex:

using MutexType = std::shared_timed_mutex;

using ReadLock = std::共享unique_lock<MutexType>;

using WriteLock = std::unique_lock<MutexType>;