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); }
};