比赛条件和解锁写

时间:2012-04-03 17:24:03

标签: c++ concurrency locking race-condition

我对竞争条件和同时写作有疑问。

我有一个类可以从不同的线程访问对象。我想只根据需要计算一些值并缓存结果。出于性能原因,我宁愿不使用锁(在任何人问之前 - 是的,它在我的情况下是相关的)。

这构成了竞争条件。但是,对象是const,不会更改。因此,如果不同的线程计算要缓存的值,它们在我的用例中保证是相同的。在没有锁定的情况下编写这些值是否安全?或者,从更广泛的角度来说,在不锁定的情况下从不同的线程向内存写入相同的内容是否安全?

写入的值是bool和double类型,所讨论的体系结构可能是x86和ARM。

编辑:向所有人表达他们的意见。我终于决定找到一种不涉及缓存的方法。这种方法看起来很像“黑客”,并且使用标志变量存在问题。

4 个答案:

答案 0 :(得分:4)

正如你所说,这是一种竞争条件。在C ++ 11下,它在技术上是数据竞争和未定义的行为。值是否相同并不重要。

如果您的编译器支持它(例如最近的gcc,或gcc或带有Just::Thread库的MSVC),那么您可以使用std::atomic<some_pod_struct>为您的数据提供原子包装(假设一个POD结构 - 如果不是那么你有更大的问题)。如果它足够小,那么编译器将使其无锁,并使用适当的原子操作。对于较大的结构,库将使用锁。

在没有原子操作或锁定的情况下执行此操作的问题是可见性。虽然x86或ARM上的处理器级别没有问题,但是从多个线程/处理器到同一个内存写入相同的数据(假设它确实是逐字节相同的),假设这是一个缓存,我希望你会想要阅读这些数据,而不是重新计算它,如果它已经被写入。因此,您需要某种标志来表示完成情况。除非您使用原子操作,锁定或合适的内存屏障指令,否则“就绪”标志可能会在数据之前对其他处理器可见。这将使事情变得非常糟糕,因为第二个处理器现在读取的是一组不完整的数据。

您可以使用非原子操作编写数据,然后使用原子数据类型作为标志。在C ++ 11下,这将生成合适的内存屏障和同步,以确保数据对于看到标志集的任何线程都是可见的。两个线程写入数据仍然是未定义的行为,但实际上可能没问题。

或者,将数据存储在由执行计算的每个线程分配的堆内存块中,并使用比较和交换操作来设置原子指针变量。如果比较和交换失败,那么另一个线程首先到达那里,所以释放数据。

答案 1 :(得分:1)

最终答案可能取决于您的数据结构。

在“非可移植”领域,您可能需要查看compare and swap,大多数处理器都允许您在指针大小的实体上执行此操作。要访问它,您可以使用内联汇编(在x86上,这些是lock cmpxchg指令),或者可能使用GCC同步扩展。在看到未初始化的值时,每个线程都可能急切地初始化,并发出比较和交换以尝试设置值。如果比较和交换失败,则意味着另一个线程击败了你。

最终,使用该操作通常最终会相当于实现自旋锁,但您可能希望避免使用...

答案 2 :(得分:1)

如果值相同,则无需防止对来自不同线程的POD变量的写入。但是,如果你有指针,你肯定应该进行互锁交换。

更新:为了澄清,对于您的情况,缓存和优化不会产生任何不利影响,因为您在所有线程上写入完全相同的值。出于同样的原因,您不需要创建变量volatile。唯一可能存在问题的是,如果您的变量未与机器的字大小对齐。有关详细信息,请参阅https://stackoverflow.com/a/54242/677131。默认情况下,变量会自动对齐,但您可以显式更改对齐。

有一种替代方法可以完全避免这个问题。由于变量具有相同的值,因此要么在并发执行开始之前预先计算它们,要么让每个线程都有自己的副本。后者具有在NUMA机器上提供更好性能的优势。

答案 3 :(得分:1)

我必须先说,使用锁定通常是正确的方法,但......

即使数据大于处理器字大小,从多个线程写入相同的变量也不会是不安全的。没有过渡状态,其中变量可能被破坏,因为至少有一个线程将完成写入值。其他线程不会通过拧过相同的值来改变它。

因此,如果有保证无论什么线程计算结果总是相同,那么多线程就没有危险。在进行计算之前,只需检查一个标志(“已计算?”)。多个线程将输入值计算代码,但一旦完成,当然没有其他线程会再这样做。显然,做n次同样的事情是浪费时间。这里的问题是,是否会使用锁定保存您的任何时间或相反?只有性能测试可以给你答案。除非有其他原因不使用锁。