复制带有互斥量的 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++ 标准保证:
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
两次(中间没有析构函数调用)对调用者而言也是违反合同的,因此通过构造函数中的互斥锁进行序列化不会解决任何问题;
- 此外,互斥体实例成员不会解决任何问题,因为在互斥体本身的初始化过程中无论如何都会出现竞争条件。要使其工作,您需要一个全局互斥锁,但您仍然无法解决上述问题。
所以,长话短说,不用担心在构造过程中锁定正在构造的对象。该语言向您保证对象构造是非并发的。
对于 类 以互斥锁作为成员,当我创建复制构造函数时,我需要决定锁定哪个互斥锁。对于下面的代码,我想知道为什么我只需要锁定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++ 标准保证:
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
两次(中间没有析构函数调用)对调用者而言也是违反合同的,因此通过构造函数中的互斥锁进行序列化不会解决任何问题; - 此外,互斥体实例成员不会解决任何问题,因为在互斥体本身的初始化过程中无论如何都会出现竞争条件。要使其工作,您需要一个全局互斥锁,但您仍然无法解决上述问题。
所以,长话短说,不用担心在构造过程中锁定正在构造的对象。该语言向您保证对象构造是非并发的。