诡异的线程同步问题(高手进)

passion_wu128 2014-05-22 11:44:26
程序如下:

#include <iostream>
#include <process.h>
#include <Windows.h>
using namespace std;

long total = 0;
const int M = 100000;

unsigned __stdcall ThreadProc(void*)
{
for(long i = 1; i <= M; i++)
total += 1;
return 0;
}

int main(int argc, char* argv[])
{
const int N = 10000;
HANDLE h[2];
int min = 0;
int max = 0;

//统计N次total的最小和最大值
for(int i = 0; i < N; i++)
{
total = 0;
h[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, 0, NULL);
h[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, 0, NULL);

//等待两个线程都结束
WaitForMultipleObjects(2, h, TRUE, INFINITE);

CloseHandle(h[0]);
CloseHandle(h[1]);

if(min == 0)
min = total;
if(total < min)
{
min = total;
}
if(total > max)
{
max = total;
}
}
cout << "(" << min << "," << max << ")";
return 0;
}


total += 1;的汇编码为:

1, mov eax,dword ptr [total (0E69148h)]
2, add eax,1
3, mov dword ptr [total (0E69148h)],eax


线程在第3行汇编码之前切换就会导致total的值不准确。
比如第一个线程在total为0时执行完第1行汇编码,然后切换到线程2。
等待线程2执行完之后再将eax的值写入到total的内存区,total的值将变成1。
线程1执行完之后total的值为M。
按我的分析total的范围应该是[M,2M]的闭区间。
可统计的结果显示min有可能小于M,尤其的是M越大越明显,很诡异吧!!!
而且我也测试过,每次都是两个线程结束后统计的min和max,所以找不到原因了,求解答。。。
...全文
587 23 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
23 条回复
切换为时间正序
请发表友善的回复…
发表回复
赵4老师 2014-12-12
  • 打赏
  • 举报
回复
仅供参考: 无profiler不要谈效率!!尤其在这个云计算、虚拟机、模拟器、CUDA、多核 、多级cache、指令流水线、多种存储介质、……满天飞的时代!
FancyMouse 2014-05-23
  • 打赏
  • 举报
回复
引用 15 楼 passion_wu128 的回复:
[quote=引用 14 楼 FancyMouse 的回复:] volatile只是跳过寄存器而已。你这里是内存的问题,volatile避免不了。 主要问题还是线程x的写什么时候能被线程y看到。最坏情况下,线程1:写了1;线程2:之前一直写到M-1,然后再看到线程1的1,最后一下+1变成2;线程1:再一直写到M-1,然后再看到线程2最后的一个2,最后一下+1变成3。然后主线程看到3。 所以M无论多大,答案是3也是有理论可能的。 当然这是多核的共享内存问题。单核上的确线程再怎么切换也不会小于M,但是那是单核世界。
你说的我懂了,可是单核上多线程也是共享全局变量的内存啊?跟多核怎么就不一样了?[/quote] 内存模型有区别。单核你只是切换的话另一个线程写的结果可以立刻被切换过去的新线程读到。多核就没这个假设了。
shiter 2014-05-23
  • 打赏
  • 举报
回复
passion_wu128 2014-05-23
  • 打赏
  • 举报
回复
引用 18 楼 PDD123 的回复:
我使用的编译器是VS 2010,编译的版本是release,结果被优化成这样了:
ThreadProc proc near
add     total, 186A0h
xor     eax, eax
retn    4
ThreadProc endp
release版本把优化禁用同样有这个现象。 或者把变量声明成volatile,total+=1的代码也不会被优化。
PDD123 2014-05-23
  • 打赏
  • 举报
回复
我使用的编译器是VS 2010,编译的版本是release,结果被优化成这样了:
ThreadProc proc near
add     total, 186A0h
xor     eax, eax
retn    4
ThreadProc endp
passion_wu128 2014-05-23
  • 打赏
  • 举报
回复
引用 3 楼 lineage101 的回复:
计算有误,分两列看数据吧 一列为totaol 值一列为寄存器 a线程 0 1 跳转b(极端例子线程切换不可估计) 计算到 9999 9999,寄存器数据写入total 跳转到a 寄存器数据存入total,total数据变为1 1,1 线程切换到b 1,1 切换到a 10000,10000,a计算结束了 切换到b,b也计算结束了 1,2 b数据写入total值为2. 所以最小值不一定为m
很有逻辑性,这样total理论上的范围是【2,M】,多谢了。
PDD123 2014-05-23
  • 打赏
  • 举报
回复
我觉得应该是14楼说的原因了。 过程: 1.线程1:i=1,读取total,eax=0 切换 2.线程2:i=1,读取total,eax=0 并一直执行到i=M-1,并保存结果,total=M-1 切换 3.线程1:i=1,eax+1,并保存 total=1 切换 4.线程2:i=M,读取total,eax=1 切换 5.线程1:一直运行到结束 6.线程2:计算并保存,于是total=2 结束…… 但是14楼说的最后那句话我不赞同:
引用 14 楼 FancyMouse 的回复:
当然这是多核的共享内存问题。单核上的确线程再怎么切换也不会小于M,但是那是单核世界。
以上分析的就是在单核上切换的呀。
passion_wu128 2014-05-23
  • 打赏
  • 举报
回复
引用 14 楼 FancyMouse 的回复:
volatile只是跳过寄存器而已。你这里是内存的问题,volatile避免不了。 主要问题还是线程x的写什么时候能被线程y看到。最坏情况下,线程1:写了1;线程2:之前一直写到M-1,然后再看到线程1的1,最后一下+1变成2;线程1:再一直写到M-1,然后再看到线程2最后的一个2,最后一下+1变成3。然后主线程看到3。 所以M无论多大,答案是3也是有理论可能的。 当然这是多核的共享内存问题。单核上的确线程再怎么切换也不会小于M,但是那是单核世界。
你说的我懂了,可是单核上多线程也是共享全局变量的内存啊?跟多核怎么就不一样了?
FancyMouse 2014-05-23
  • 打赏
  • 举报
回复
volatile只是跳过寄存器而已。你这里是内存的问题,volatile避免不了。 主要问题还是线程x的写什么时候能被线程y看到。最坏情况下,线程1:写了1;线程2:之前一直写到M-1,然后再看到线程1的1,最后一下+1变成2;线程1:再一直写到M-1,然后再看到线程2最后的一个2,最后一下+1变成3。然后主线程看到3。 所以M无论多大,答案是3也是有理论可能的。 当然这是多核的共享内存问题。单核上的确线程再怎么切换也不会小于M,但是那是单核世界。
食财物权情性 2014-05-23
  • 打赏
  • 举报
回复
我正好也是双核四线程的机子。 如果改成如下,我发现区间一直是【m,2m】。 求其他人运行证实: SetThreadAffinityMask(h[0], 2); --------》 SetThreadAffinityMask(h[0], 1); SetThreadAffinityMask(h[1], 4); --------》 SetThreadAffinityMask(h[0], 2); 有且只有改成1和2的时候才是这结果。难道是巧合?
passion_wu128 2014-05-22
  • 打赏
  • 举报
回复
引用 1 楼 hordemark 的回复:
http://blog.csdn.net/morewindows/article/details/7429155
改用InterlockedIncrement函数当然没问题,看清楚我的问题好么?
hordemark 2014-05-22
  • 打赏
  • 举报
回复
http://blog.csdn.net/morewindows/article/details/7429155
passion_wu128 2014-05-22
  • 打赏
  • 举报
回复
引用 8 楼 go_and_see 的回复:
问题在这里:

        h[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, 0, NULL);
        h[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, 0, NULL);
这两行代码是向操作系统申请开启两个线程,系统什么时候开启两个线程则不是确定的。不能假定两个线程一定是同时开启,所以全局变量的值是随机的。
再怎么随机按线程切换的过程也不可能小于M啊!!! 本来我怀疑是去Cache惹的祸,但将total声明为volatile变量还是有这个现场。 所以无语了………………
passion_wu128 2014-05-22
  • 打赏
  • 举报
回复
引用 5 楼 Symfund 的回复:
你到底想表达什么?两个线程对一个共享变量进行操作,神马情况都有可能出现!这不正是你想要的吗?随机现象,不可确定。
是随机现象,但按线程切换的过程再怎么随机也不可能出现我说的现象啊。所以我想问为什么会这样。
passion_wu128 2014-05-22
  • 打赏
  • 举报
回复
引用 4 楼 Fire_Lord 的回复:
上图!!
大哥,你的图里面eax怎么被两个线程共有了??? 线程切换的时候肯定会保留现场啊,所有的寄存器都会保存起来,再次执行的时候再恢复。 如果所有线程都用同一份寄存器变量,那不全乱套了???程序还能跑么???
a7301048a 2014-05-22
  • 打赏
  • 举报
回复
这个是很正常滴 两个线程执行有快慢,但是不是一定快和一定慢,自己琢磨这句话吧
边走边瞧 2014-05-22
  • 打赏
  • 举报
回复
楼上几个说线程切换的同学,请动手试验后再发表意见。下面是俺的代码,双核四线程,两个线程分别绑定在不同的物理核心上,最后的结论和LZ一致。请问,你们说的线程切换考虑过多核心CPU吗?

h[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, CREATE_SUSPENDED, NULL);
h[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, CREATE_SUSPENDED, NULL);
SetThreadAffinityMask(h[0], 2);
SetThreadAffinityMask(h[1], 4);
::ResumeThread(h[0]);
::ResumeThread(h[1]);

边走边瞧 2014-05-22
  • 打赏
  • 举报
回复
问题在这里:

        h[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, 0, NULL);
        h[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, 0, NULL);
这两行代码是向操作系统申请开启两个线程,系统什么时候开启两个线程则不是确定的。不能假定两个线程一定是同时开启,所以全局变量的值是随机的。
zhousitiaoda 2014-05-22
  • 打赏
  • 举报
回复
楼上的字写的不错,赞一个。
Fire_Lord 2014-05-22
  • 打赏
  • 举报
回复
引用 4 楼 Fire_Lord 的回复:
上图!!
怎么是横着的,楼主点击一下看大图就是正常的了。
加载更多回复(3)

65,187

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧