这段代码是线程安全的吗?

时间:2009-07-28 08:00:47

标签: c++ thread-safety lock-free

这是我目前正在维护的一些代码的简化版本:

int SomeFunc() 
{
  const long lIndex = m_lCurrentIndex;
  int nSum = 0;
  nSum += m_someArray[lIndex];
  nSum += m_someArray[lIndex];
  return nSum;
}

lCurrentIndex由另一个线程定期更新。问题是;将制作m_CurrentIndex的本地副本,确保对m_someArray的两次访问都使用相同的索引吗?

请注意,这是一个简化的例子;我正在考虑制作本地副本的概念,而不是这里显示的确切代码段。我知道编译器会将值放在寄存器中,但这仍然是本地副本,而不是从lCurrentIndex读取两次。

谢谢!

编辑:初始分配是安全的,在我们的设置中保证两者都是32位。 Edit2:它们在32位边界上正确对齐(忘了那个)

7 个答案:

答案 0 :(得分:15)

不,读取共享变量的本地的初始化不一定是原子的。 (例如,考虑在8位平台上需要什么代码)通常,编写线程安全代码的唯一方法是使用编译器和/或OS指定的原子操作,或使用操作系统锁定功能。 / p>

答案 1 :(得分:6)

  

将制作本地副本   m_CurrentIndex确保两次访问   到m_someArray使用相同的索引?

在同一个SomeFunc的执行中,是的,当然。局部整数变量(lIndex)不会在函数中间神奇地改变它的值。

当然,以下情况也是如此:m_someArray[lIndex]的实际值(与lIndex的实际值相反)可能会发生变化; m _ someArray本身可能会改变;和what Neil said about the validity of lIndex's initial value

答案 2 :(得分:3)

是的,当前索引的副本将确保两个数组访问使用相同的索引。然而,这并不是我想到的“线程安全”。您需要关注并发访问共享变量。对我来说,看起来像访问阵列也可能是一个潜在关注的领域。

答案 3 :(得分:3)

这应该是线程安全的(至少它会在我使用的所有编译器/操作系统上)。但是,要更加确定,您可以将m_lCurrentIndex声明为volatile。然后编译器会知道它可能随时发生变化。

答案 4 :(得分:1)

这里要问的另一个问题是:是从m_lCurrentIndexlIndex的原子操作的复制操作吗?如果不是,你最终可能会使用非常奇怪的值,这些值可能没什么用。 :)

要点是:当您使用多个线程时,将无法绕过某种锁定或同步。

答案 5 :(得分:1)

本地副本的概念

  

我正在考虑制作本地副本的概念,而不是这里显示的确切代码。

如果不了解更多细节,这个问题就无法回答。如果将m_lCurrentIndex“制作本地副本”放入lIndex是原子的,那么归结为问题。

假设x86并假设m_lCurrentIndex是DWORD对齐的并且假设long表示DWORD(在大多数x86编译器中都是如此),那么是的,这是原子的。假设x64并且假设long表示DWORD并且m_lCurrentIndex是DWORD对齐或long表示64b字而m_lCurrentIndex再次对齐64b是,这是原子的。在其他平台上或没有对齐保证,副本可能需要两次或更多次物理读取。

即使没有本地副本是原子的,你仍然可以使用CAS样式循环使其无锁和线程安全(乐观并假设您可以在没有锁定的情况下执行操作检查一切是否正常,如果没有,回滚并再次尝试),但它可能会做更多的工作,结果将是无锁,但不是等待。

记忆障碍

提醒一句:一旦向前移动一步,您很可能会同时处理多个变量,或者处理共享变量,这些变量充当指针或索引以访问其他共享变量。在这一点上,事情会变得越来越复杂,因为从那时起你需要考虑诸如读/写重新排序和内存障碍之类的事情。另请参阅How can I write a lock free structure

答案 6 :(得分:0)

是的,它将访问数组的相同元素。就像您正在将m_lCurrentIndex的值快照到局部变量中。由于局部变量有自己的内存,无论你对m_lCurrentIndex做什么都不会对局部变量产生影响。但请注意,由于分配操作不能保证是原子的,因此最终可能会在lIndex中使用无效值。如果从一个线程更新m_lCurrentIndex并同时从另一个线程尝试分配到lIndex,则会发生这种情况。