C++ 类中的每个setter是否都应该有一个互斥锁,或者使所有成员变量都是原子的,以保证线程安全?
我正在写一个有很多可设置变量的类。现在的要求是类应该是线程安全的。因此,据我所知,我的选择是使用互斥锁或使变量原子化(或两者兼而有之)。关于这件事我有几个问题,因为我不知道该怎么办C++ 类中的每个setter是否都应该有一个互斥锁,或者使所有成员变量都是原子的,以保证线程安全?,c++,thread-safety,C++,Thread Safety,我正在写一个有很多可设置变量的类。现在的要求是类应该是线程安全的。因此,据我所知,我的选择是使用互斥锁或使变量原子化(或两者兼而有之)。关于这件事我有几个问题,因为我不知道该怎么办 我是否应该使所有成员变量(范围从int、float、bools到std::map等)成为原子变量?然后避免在setter和getter中使用互斥体,除非它们有比简单赋值更多的逻辑 我应该为所有setter创建单独的互斥体,还是应该为所有相关的操作(例如,添加/读取/删除用户、获取用户计数、设置用户计数等)使用单个互
- 我是否应该使所有成员变量(范围从int、float、bools到std::map等)成为原子变量?然后避免在setter和getter中使用互斥体,除非它们有比简单赋值更多的逻辑
- 我应该为所有setter创建单独的互斥体,还是应该为所有相关的操作(例如,添加/读取/删除用户、获取用户计数、设置用户计数等)使用单个互斥体
在性能方面,同时使用互斥和原子成员变量是一个坏主意吗?也就是说,假设我将我的
statusChanged
变量设置为原子变量,并且它被用于具有一些逻辑的方法中,因此我使用作用域的_锁使整个块线程安全。那会不会太过分了?互斥锁足够吗?线程安全不是类的属性。线程安全是两段代码和数据之间的关系属性
基于互斥的排除策略和原子策略都有一个问题,那就是它们不能组合。“A”可以是线程安全的,“B”可以是线程安全的,“A+B”不能是线程安全的
一个简单的例子是一个对象,它有一个属性x
,读写器锁保护setter和getter方法。做一些像obj.x=obj.x+2这样简单的事情——编写读写操作——不能“线程安全地”工作;之后,x不能比之前大2
如果您使x
原子化并开始执行CAS类型的“原子”操作,则可以获得x+=2
线程安全;但是,如果您有两个字段x
和y
,那么使用另一个线程计算y-x
无法安全地完成简单的组合x=y+2
基本上,互斥保护的访问器或原子公开的数据并不能保证线程安全。许多合理的操作都不合理
您可以完全退出这个模型——使用消息队列和不可变的共享数据等——但这可能超出范围。因此,如果您坚持“线程安全的可变对象”的想法,您应该这样做:
template<class T>
struct mutex_guarded {
template<class F>
auto read(F&& f)const{
auto l=lock();
return f(t);
}
template<class F>
auto write(F&& f){
auto l=lock();
return f(t);
}
mutex_guarded(T tin):t(std::move(tin)){}
mutex_guarded()=default
private:
mutable std::mutex m;
T t;
auto lock() const { return std::unique_lock<std::mutex>(m); }
};
线程安全不是类的属性。线程安全是两段代码和数据之间的关系属性
基于互斥的排除策略和原子策略都有一个问题,那就是它们不能组合。“A”可以是线程安全的,“B”可以是线程安全的,“A+B”不能是线程安全的
一个简单的例子是一个对象,它有一个属性x
,读写器锁保护setter和getter方法。做一些像obj.x=obj.x+2这样简单的事情——编写读写操作——不能“线程安全地”工作;之后,x不能比之前大2
如果您使x
原子化并开始执行CAS类型的“原子”操作,则可以获得x+=2
线程安全;但是,如果您有两个字段x
和y
,那么使用另一个线程计算y-x
无法安全地完成简单的组合x=y+2
基本上,互斥保护的访问器或原子公开的数据并不能保证线程安全。许多合理的操作都不合理
您可以完全退出这个模型——使用消息队列和不可变的共享数据等——但这可能超出范围。因此,如果您坚持“线程安全的可变对象”的想法,您应该这样做:
template<class T>
struct mutex_guarded {
template<class F>
auto read(F&& f)const{
auto l=lock();
return f(t);
}
template<class F>
auto write(F&& f){
auto l=lock();
return f(t);
}
mutex_guarded(T tin):t(std::move(tin)){}
mutex_guarded()=default
private:
mutable std::mutex m;
T t;
auto lock() const { return std::unique_lock<std::mutex>(m); }
};
没有一个正确的方法可以做到这一点。这取决于类的细节以及如何使用。如果不问一个更具体的问题,你就不会得到有用的答案。例如,如果它是一个通常只有一个实例的类,那么解决方案可能与您使用的有成千上万个实例的类完全不同。谢谢,但我认为我们不应该对用户如何使用我们的类进行任何假设。我错了吗?它应该被实例化一次,但由于我刚才所说的(不假设clinet如何使用您的代码),我在这方面没有得到具体的说明。原子变量和互斥体并不是彼此的替代品。它们有不同的语义,使用方式也不同。为了做出智能决策,您必须弄清楚需要什么样的多线程语义。如果线程间排序很复杂,那么使用互斥体几乎是唯一的选择。原子变量本身没有互斥量那样复杂的排序。但如果原子序列足够,它们将有更少的开销来处理。你需要什么样的回答是你自己需要弄清楚的。然后我不知道该告诉你什么。如何使同步原语线程安全,如何使数据库线程安全,以及如何使配置容器类线程安全都是完全不同的。通常,正确的答案只是让调用代码担心它,因为调用代码有关于类如何使用的更多信息。也可能是您完全错误地处理了问题,并且线程安全可以通过不同的体系结构来避免。但你提供的信息很少,很难判断。没有一般的经验法则,没有一种正确的方法可以做到这一点。这取决于类的细节以及如何使用。如果不问一个更具体的问题,你就不会得到有用的答案。例如,如果它是一个通常只有一个实例的类,那么解决方案可能与您使用的c类完全不同
template<class T>
struct shared_mutex_guarded {
template<class F>
auto read(F&& f)const{
auto l=lock();
return f(t);
}
template<class F>
auto write(F&& f){
auto l=lock();
return f(t);
}
shared_mutex_guarded(T tin):t(std::move(tin)){}
shared_mutex_guarded()=default
private:
mutable std::shared_mutex m;
T t;
auto lock() { return std::unique_lock<std::shared_mutex>(m); }
auto lock() const { return std::shared_lock<std::shared_mutex>(m); }
};