好的,我有一个关于线程的问题。
有两个未同步的线程同时运行并使用全局资源“int num” 第一:
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 <windows.h>
#include <process.h>
#include <stdio.h>
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");
}
答案 0 :(得分:21)
num++
和num--
不必是原子操作。以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
循环,如果它感觉如此倾向!
答案 1 :(得分:2)
num
的可能值包括所有可能的int
值,以及鼻子恶魔的浮点值,字符串和jpeg。一旦您调用未定义的行为,所有投注都将关闭。
更具体地说,在没有同步的情况下从多个线程修改同一对象会导致未定义的行为。在大多数真实世界的系统中,您看到的最差效果可能会丢失或加倍或减少,但可能会更糟(内存损坏,崩溃,文件损坏等)。所以就是不要这样做。
下一个即将推出的C和C ++标准将包括可以从多个线程安全地访问 的原子类型,而无需任何同步API。
答案 2 :(得分:1)
您说的是同时运行的线程,如果您的系统中只有一个核心,则实际情况可能并非如此。我们假设您有多个。
如果多个设备可以以CPU或总线主控或DMA的形式访问主存储器,则必须同步它们。这由锁前缀处理(对于指令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”指令构造的。一旦失败(没有设置自旋锁&lt; =&gt;成功设置它),spinlock_leave将只用“mov mem,0”分配给它。 “++ n”计算重试总次数。
我将循环更改为250万(因为每个循环有两个线程和两个自旋锁,我得到1000万个自旋锁,漂亮且易于圆形)并且在双核Athlon II上使用“rdtsc”计数时序M300 @ 2GHz,这就是我发现的
因此,不受另一个CPU影响的自旋锁的附加物增加了几百个总执行时间。 num中的最终值为0.
顺便提一下,没有自旋锁的两个线程的171157957周期与两个自旋锁的线程相比非常有利,其中自旋锁时间已被移除:4099370103-3930091465 = 169278638个周期。
对于我的序列,自旋锁竞争导致每个线程重试21-29百万次,每个自旋锁重试4.2-5.8次,每个自旋锁重启5.2-6.8次。增加自旋锁导致执行时间损失为1927%(1500 / 74-1)。最慢的螺旋锁需要5-8%的尝试次数。
答案 3 :(得分:0)
正如托马斯所说,结果是不可预测的,因为你的增量和减量是非原子的。您可以使用InterlockedIncrement和InterlockedDecrement(它们是原子的)来查看可预测的结果。