线程同步 - 微妙的问题

时间:2009-12-17 15:21:00

标签: c++ multithreading synchronization thread-safety

让我有这个循环:

static a;
for (static int i=0; i<10; i++)
{
   a++;
   ///// point A
}

到这个循环2个线程进入......

我不确定某事......如果thread1进入POINT A会发生什么,保持在那里,而THREAD2进入循环10次,但是在将第10个循环增加到10之后,在检查i的值之前,如果它小于10, Thread1正在退出循环并假设增加i并再次进入循环。 Thread1将增加的价值是什么(我将会看到)?它会是10还是0?

是否可以认为Thread1会将i递增为1,然后线程2将再次循环9次(并且它们可能是8,7等等)

感谢

11 个答案:

答案 0 :(得分:5)

如果多个线程之间共享i,则所有投注均已关闭。在另一个线程的执行过程中,任何线程都可以在任何点上递增i(包括该线程的递增操作的中途)。在上面的代码中没有任何有意义的方法来推理i的内容。不要那样做。要么为每个线程提供自己的i副本,要么增加和比较10个单个原子操作。

答案 1 :(得分:5)

你必须意识到增量操作实际上是真的:

read the value
add 1
write the value back

你必须问自己,如果其中两个同时发生在两个独立的线程中会发生什么:

static int a = 0;

thread 1 reads a (0)
adds 1 (value is 1)
thread 2 reads a (0)
adds 1 (value is 1)
thread 1 writes (1)
thread 2 writes (1)

对于两个同时增量,您可以看到其中一个可能会丢失,因为两个线程都读取了预先递增的值。

您提供的示例因静态循环索引而变得复杂,我最初没有注意到。 由于这是c ++代码,标准实现是所有线程都可以看到静态变量,因此所有线程只有一个循环计数变量。理所当然的事情是使用普通的自动变量,因为每个线程都有自己的,不需要锁定。

这意味着虽然有时你会失去增量,但你也可能获得它们,因为循环本身可能会丢失计数并重复多次。总而言之,这是不该做的一个很好的例子。

答案 2 :(得分:3)

这不是一个微妙的问题,因为如果同步成为问题,你绝不会在实际代码中允许这样做。

答案 3 :(得分:2)

我将在你的循环中使用i++

for (static int i=0; i<10; i++)
{
}

因为它模仿a。 (注意,static这里很奇怪)

考虑线程A是否在到达i++时被暂停。线程B一直获得i到9,进入i++并使其成为10.如果它继续前进,则循环将存在。啊,但现在线程A恢复了!所以它从中断的地方继续:增加i!所以i变成11,你的循环被塞进去了。

线程共享数据时,需要对其进行保护。如果您的平台支持,您还可以使i++i < 10以原子方式(永不被中断)发生。

答案 4 :(得分:1)

您应该使用mutual exclusion来解决此问题。

答案 5 :(得分:1)

这就是为什么在多线程环境中,我们假设使用锁。

在你的情况下,你应该写:

bool test_increment(int& i)
{
  lock()
  ++i;
  bool result = i < 10;
  unlock();
  return result;
}

static a;
for(static int i = -1 ; test_increment(i) ; )
{
   ++a;
   // Point A
}

现在问题消失了。请注意lock()unlock()应该锁定和解锁所有尝试访问i的线程共有的互斥锁!

答案 6 :(得分:0)

是的,任何一个线程都可以完成该循环中的大部分工作。但正如Dynite解释的那样,这将(并且应该)永远不会出现在实际代码中。如果同步是一个问题,您应该提供互斥(Boost,pthread或Windows线程)互斥,以防止此类竞争条件。

答案 7 :(得分:0)

为什么要使用静态循环计数器?

这有点像家庭作业,而且很糟糕。

答案 8 :(得分:0)

两个线程都有自己的i副本,因此行为可以是任何东西。这就是为什么会出现这样一个问题。

当您使用互斥锁或临界区时,线程通常会同步,但如果变量不是易失性的话,即使这样也不是绝对保证。

毫无疑问,有人会指出“volatile在多线程中毫无用处!”但人们说很多蠢事。你不必有波动,但它对某些事情有帮助。

答案 9 :(得分:0)

如果你的“int”不是原子机器字大小(想想64位地址+模拟32位VM的数据),你将"word-tear"。在这种情况下,你的“int”是32位,但是机器以原子方式寻址64。现在你必须读取全部64,增加一半,并将它们全部写回。

这是一个更大的问题;如果你真的想要血淋淋的细节,可以在处理器指令集上使用gret gcc,以及如何在任何地方实现“volatile”。

添加“volatile”并查看机器代码的更改方式。如果您不是低头看芯片寄存器,请使用boost库并完成它。

答案 10 :(得分:0)

如果需要同时增加多个线程的值,则查找“原子操作”。对于linux,查找“gcc atomic operations”。大多数平台都有硬件支持,可以进行原子递增,添加,比较和交换等。对于这个原因,锁定会过于强大......原子公司比锁定公司解锁更快。如果您必须同时更改大量字段,则可能需要锁定,尽管您可以使用大多数原子操作一次更改128位字段。

volatile与原子操作不同。 Volatile帮助编译器知道何时使用变量副本是个坏主意。在其使用中,当您有多个线程更改数据时,volatile非常重要,您希望在没有锁定的情况下读取“最新版本”。 Volatile仍然无法修复你的a ++问题,因为两个线程可以同时读取“a”的值,然后两个都增加相同的“a”,然后最后一个写“a”获胜并且你丢失了一个inc。易失性会降低优化代码的速度,因为不要让编译器在寄存器中保存值而不是。