我们需要对一个只有read方法的类进行线程安全设计吗



我知道在C++中,为了防止多线程环境中的数据竞争,我们可以在类中添加一个mutex

但是,如果有下面这样一个简单的类,它只有一个get()方法,我们还需要考虑线程安全问题吗?

class SimpleClass {
public:
SimpleClass(int val) : v(val) {};
int get() { return v; }
private:
int v;
};

您的代码不安全,并且存在潜在的竞争条件。

class SimpleClass {
public:
SimpleClass(int val) : v(val) {};
int get() { return v; }
private:
int v;
};
void thread_1(SimpleClass& sc)
{
std::cout << sc.get() << 'n';
}
void thread_2(SimpleClass& sc)
{
SimpleClass other(5);
sc = other; // potential race
}

问题是编译器生成了一个赋值运算符,允许类的对象被分配来覆盖它们的内部数据。

这会引发一场潜在的竞争。

如果这确实是整个类,并且在创建实例后无法更改v的值,则该类是不可变的,并且不需要任何其他保护措施。无论哪个线程调用get,在任何时刻,都将获得与实例初始化时相同的值。这里没有比赛条件的可能性。

要使代码不安全,必须满足四个条件。第三种情况只有在代码包含写入或更新时才会发生。

另请参阅This so answer:

  1. 必须存在可从多个线程访问的内存位置
  2. 代码中存在与这些共享内存位置相关联的某些属性(通常称为不变量),该属性必须为true或有效,程序才能正常运行
  3. 第三,在代码的某些部分(写入或实际更新)中,这个不变属性不成立(它是错误的或不正确的)。(在处理的某些部分,它暂时无效或为false)
  4. 竞争必须发生的第四个也是最后一个条件(因此代码不"线程安全")是,当不变量被破坏时,另一个线程必须能够访问共享内存,从而导致不一致或不正确的行为

在您的情况下,考虑以下[pudo]代码:

create new SimpleClass(1) in variable a
create new SimpleClass(2) in variable b
Switch a and b
{
create SimpleClass(a) into variable temp <-- with value 1
a=b                   <-- puts reference to b into variable a
b=temp                <-- puts temp(value = 1) into variable b
}

如果这个代码被中间的第二个线程中断(在b被分配给a之后,但在temp被分配给b之前),这将是不好的。

编辑。(为了澄清@Juan在下面提出的观点)。因此,在您的情况下,(这个SimpleClass的情况,是的,因为类是不可变的,所以它本身是"线程安全的",因为其中的代码不能在类本身内引起竞争。但这并不意味着该类不能在外部多线程代码中使用,从而引发竞争条件。

不可变类本质上是线程安全的。

粗略地说,当写入可以与同一数据的任何其他读取或写入同时运行时,就会出现并发问题。

想想共享锁和独占锁。读取是通过获取共享锁来执行的,而写入则需要获取独占锁。任何数量的线程都可以同时拥有一个共享锁。只有一个线程可以同时拥有独占锁,并且在持有独占锁时不能持有共享锁。这意味着您可以同时执行读取,但不能执行写入、读取和写入。如果您的数据永远无法修改,那么就不会出现并发问题(不需要独占锁,因此共享锁毫无意义)。

这是函数式语言的优势之一:数据永远不会被修改,使函数本质上是线程安全的,并允许积极的编译器优化。

现在,还有一个关于线程安全的问题通常被遗忘:内存模型,特别是在现代NUMA体系结构中。

如果你知道volatile变量,关键是只要程序保持正确,编译器就可以自由地优化数据访问。。。。在单线程处理中。

如果编译器不知道另一个线程可能同时读取或写入变量,它可能会将该值保留在寄存器中,并且从不检查主内存中的更改。对于不同级别的缓存中的缓存值,也可能发生这种情况。如果它在编译时知道条件的结果,而不知道所涉及的值可能会发生不确定性的变化,那么它甚至可以优化条件分支。

声明一个变量volatile表示它的值可能会改变,每次都会强制刷新到主内存,并从主内存中读取。

但是,如果价值从未改变,为什么需要这样做呢?嗯,值在构造过程中会发生变化,不能假设它是瞬时的或原子的。如果编译器不知道它是多线程的,它甚至可能永远不会将任何数据刷新到主存中。如果您使对该对象的引用可用于另一个线程,它将从从未初始化过的主内存中读取该对象。或者它甚至可以看到正在进行的初始化(在旧版本的java中初始化一个大字符串时可能会发生这种情况)。

我相信现代C++标准定义了内存模型,但我还没有深入研究它。如果内存模型未指定或不够强,则可能始终需要执行原语,例如获取或释放锁,从而建立"先发生后发生"的关系。在任何情况下,您肯定需要告诉编译器数据是可变的或不可变的,这样它就可以为使用中的内存模型提供保护。

在这种情况下,我会用const修饰符声明变量和getter方法。我确信它会很好地工作,但我建议研究你正在使用的标准的记忆模型,如果需要,可以切换到更现代的标准。

最新更新