复制带有互斥量的 ctor 作为数据成员

Copy ctor with mutex as data member

对于 类 以互斥锁作为成员,当我创建复制构造函数时,我需要决定锁定哪个互斥锁。对于下面的代码,我想知道为什么我只需要锁定rhs.mu_而不必锁定this->mu_ ?是否有可能多个线程为同一个对象调用复制构造函数?

class Obj {
 public:
  std::mutex mu_;
  std::string data_;

  // copy ctor
  Obj(const Obj& rhs) {
    std::unique_lock<std::mutex> lk(rhs.mu_); // why only lock rhs.mu_?
    data_ = rhs.data_;
  }
}

更新: 这段代码是否使用new同时调用copy ctor?

Obj* t = nullptr;
Obj someObj;

// ... populate someObj

std::thread t1([&]() { t = new Obj(someObj); });
std::thread t2([&]() { t = new Obj(someObj); });

如果你的变量是局部变量,在构造过程中其他线程无法访问它,因为其他线程无法命名本线程中的局部变量。

如果你的变量有静态生命周期,线程安全由 C++ 标准保证:

[stmt.dcl]

Dynamic initialization of a block-scope variable with static storage duration or thread storage duration is performed the first time control passes through its declaration;

...

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

因此构造函数不会被调用两次。

您需要锁定复制源对象的互斥锁(以确保您正在以一致的状态复制对象),而您可以单独保留正在构造的对象的锁。

构造函数通常是这样的:根据定义,构造函数调用是单线程的:

  • 局部变量是当前线程的局部变量(因为堆栈是每个线程的);每当特定线程进入相关范围时,就会构造它们的实例;
  • 线程局部变量是线程局部的(duh),每个线程局部实例的构造发生在相关线程中;
  • 静态存储持续时间变量(globals, static locals, static class fields)保证被标准初始化一次(编译器注入类似[=12的东西=]);
  • 通过 "regular" new 分配的对象同样是安全的,因为每个线程 运行 使用像 new A 这样的表达式将分配一个不同的实例,构造函数在该实例上将 运行.

您可能遇到的唯一问题是,如果您的某些客户玩 placement new,但在这种情况下,我认为调用者有责任不同时调用 placement new 构造函数在同一对象上 - 如:

  • 即使在非并发的情况下,在同一内存位置调用 placement new 两次(中间没有析构函数调用)对调用者而言也是违反合同的,因此通过构造函数中的互斥锁进行序列化不会解决任何问题;
  • 此外,互斥体实例成员不会解决任何问题,因为在互斥体本身的初始化过程中无论如何都会出现竞争条件。要使其工作,您需要一个全局互斥锁,但您仍然无法解决上述问题。

所以,长话短说,不用担心在构造过程中锁定正在构造的对象。该语言向您保证对象构造是非并发的。