C,C ++非同步线程返回奇怪的结果

好的,我有一个关于线程的问题。

有两个未同步的线程同时运行并使用全局资源“int num”1st:

void Thread() { int i; for ( i=0 ; i < 100000000; i++ ) { num++; num--; } } 

第二:

  void Thread2() { int j; for ( j=0 ; j < 100000000; j++ ) { num++; num--; } } 

问题陈述:程序结束时变量“num”的可能值是什么。 现在我会说0将是程序结束时num的值,但是,尝试运行此代码,你会发现结果是随机的,我不明白为什么?

完整代码:

  #include  #include  #include  int static num=0; void Thread() { int i; for ( i=0 ; i < 100000000; i++ ) { num++; num--; } } void Thread2() { int j; for ( j=0 ; j < 100000000; j++ ) { num++; num--; } } int main() { long handle,handle2,code,code2; handle=_beginthread( Thread, 0, NULL ); handle2=_beginthread( Thread2, 0, NULL ); while( (GetExitCodeThread(handle,&code)||GetExitCodeThread(handle2,&code2))!=0 ); TerminateThread(handle, code ); TerminateThread(handle2, code2 ); printf("%d ",num); system("pause"); } 

num++num--不必是primefaces操作。 以num++为例,这可能是这样实现的:

 int tmp = num; tmp = tmp + 1; num = tmp; 

其中tmp保存在CPU寄存器中。

现在让我们说num == 0 ,两个线程都尝试执行num++ ,并且操作交错,如下所示:

 Thread A Thread B int tmp = num; tmp = tmp + 1; int tmp = num; tmp = tmp + 1; num = tmp; num = tmp; 

最后的结果将是num == 1即使它应该增加两次。 这里丢失了一个增量; 以同样的方式,减量也可能会丢失。

在病态情况下,一个线程的所有增量都可能丢失,导致num == -100000000 ,或者一个线程的所有减少都可能丢失,从而导致num == +100000000 。 甚至可能有更多的极端情景潜伏在那里。

然后还有其他业务正在进行,因为num未被声明为volatile。 因此,两个线程都会假设num的值不会改变,除非它们是改变它的那个。 如果感觉如此倾向,这允许编译器优化掉整个for循环!

num的可能值包括所有可能的int值,以及鼻子恶魔的浮点值,字符串和jpegs。 一旦调用未定义的行为 ,所有的赌注都将被取消。

更具体地说,在没有同步的情况下从多个线程修改同一对象会导致未定义的行为。 在大多数真实世界的系统中,您看到的最差效果可能会丢失或加倍或减少,但可能会更糟(内存损坏,崩溃,文件损坏等)。 所以就是不要这样做。

下一个即将推出的C和C ++标准将包括可以从多个线程安全访问的primefaces类型,而无需任何同步API。

您说的是同时运行的线程,如果您的系统中只有一个核心,则实际情况可能并非如此。 我们假设您有多个。

在多个设备可以以CPU或总线主控或DMA的forms访问主存储器的情况下,它们必须同步。 这由锁前缀处理(对于指令xchg是隐式的)。 它访问系统总线上的物理线路,该物理线路基本上向所有存在的设备发出信号以避开。 例如,它是Win32函数EnterCriticalSection的一部分。

因此,在同一芯片上的两个内核访问相同位置的情况下,结果将是未定义的,考虑到应该发生一些同步,因为它们共享相同的L3高速缓存(如果有的话),这可能看起来很奇怪。 似乎是合乎逻辑的,但它不起作用。 为什么? 因为当您在不同芯片上具有两个核心(即没有共享L3缓存)时会发生类似情况。 你不能指望它们是同步的。 那你可以考虑所有其他设备都可以访问主内存。 如果您计划在两个CPU芯片之间进行同步,则无法停止 – 您必须执行全面同步,阻止所有具有访问权限的设备,并确保成功同步所有其他设备需要时间来识别同步具有被请求并且需要很长时间,特别是如果设备已被授予访问权限并且正在执行必须允许完成的总线主控操作。 PCI总线将每0.125 us(8 MHz)执行一次操作,并考虑到你的CPU运行400次,你正在看很多等待状态。 然后考虑可能需要几个PCI时钟周期。

您可能会认为应该存在中型(仅存储器总线)锁,但这意味着每个处理器上都有一个额外的引脚,而每个芯片组中的附加逻辑只是为了处理一个对程序员来说真的是一个误解的情况。 所以它没有实现。

总结一下:处理你的情况的通用同步会使你的PC无用,因为它总是必须等待最后一个设备检入并确定同步。 这是一个更好的解决方案,让它成为可选的,只有当开发人员确定绝对必要时才插入等待状态。


这非常有趣,我用示例代码玩了一点,并添加了自旋锁,看看会发生什么。 自旋锁组件是

 // prototypes char spinlock_failed (spinlock *); void spinlock_leave (spinlock *); // application code while (spinlock_failed (&sl)) ++n; ++num; spinlock_leave (&sl); while (spinlock_failed (&sl)) ++n; --num; spinlock_leave (&sl); 

spinlock_failed是围绕“xchg mem,eax”指令构建的。 一旦失败(没有设置自旋锁<=>成功设置它),spinlock_leave将只用“mov mem,0”分配给它。 “++ n”计算重试总次数。

我将循环更改为250万(因为每个循环有两个线程和两个自旋锁,我获得了1000万个自旋锁,很好且易于圆化)并且在双核Athlon II M300 @ 2GHz上计时“rdtsc”计数这就是我发现的

  • 运行一个没有定时的线程(主循环除外)和锁定(如原始示例中所示)33748884 <=> 16.9 ms => 13.5个循环/循环。
  • 运行一个线程,即没有其他核心尝试210917969个循环<=> 105.5 ms => 84,4个循环/循环<=> 0.042 us / loop。 自旋锁每个自旋锁序列需要112581340个循环<=> 22.5个循环。 尽管如此,最慢的自旋锁需要1334208个循环:那是667 us或每秒仅1500。

因此,不受另一个CPU影响的自旋锁的附加因素增加了几百%的总执行时间。 num中的最终值为0。

  • 在没有自旋锁的情况下运行两个线程需要171157957个周期<=> 85.6 ms => 68.5个周期/循环。 Num包含10176。
  • 带螺旋锁的两个螺纹需要4099370103 <=> 2049 ms => 1640个循环/循环<=> 0.82 us / loop。 自旋锁需要3930091465个循环=>每个自旋锁定序列786个循环。 最慢的自旋锁需要27038623个周期:即13.52 ms或每秒仅74。 Num包含0。

顺便提一下,没有自旋锁的两个线程的171157957周期与具有自旋锁的两个线程相比非常有利,其中自旋锁时间已被移除:4099370103-3930091465 = 169278638个周期。

对于我的序列,自旋锁竞争导致每个线程重新进行21-29百万次重试,每个自旋锁重复4.2-5.8次或每个自旋锁重复5.2-6.8次。 增加螺旋锁导致执行时间损失为1927%(1500 / 74-1)。 最慢的螺旋锁需要5-8%的所有尝试。

正如托马斯所说,结果是不可预测的,因为你的增量和减量是非primefaces的。 您可以使用InterlockedIncrement和InterlockedDecrement(它们是primefaces的)来查看可预测的结果。

  • 互锁变量访问(MSDN)