对于“多线程访问同一个变量是否需要加锁”的研究

jackson35296 2010-12-17 02:22:10
加精
对于多线程访问同一变量是否需要加锁的问题,先前大家都讨论过。今天用代码验证了一下之前的猜想:32位CPU与内存的最小交换数据为4字节/次,这也是结构体要对齐4字节的原因。在物理上,CPU对于同一4字节的内存单元,不可能写2个字节的同时,又读了3字节。

测试环境为:

XEON 2CPU*2
Windows7

采用50,50,50线程交叉读写,试验代码如下:

int g_test;
int temp;
BOOL g_bRunning;
DWORD WINAPI thWriteProc1(LPVOID lParam)
{
while(g_bRunning)
{
g_test = 12345678;
Sleep(1);
}
return 0;
}
DWORD WINAPI thWriteProc2(LPVOID lParam)
{
while(g_bRunning)
{
g_test = 13579246;
Sleep(1);
}
return 0;
}

DWORD WINAPI thReadProc(LPVOID lParam)
{
while(g_bRunning)
{
temp = g_test;//读取值
if ( temp != 12345678 && temp != 13579246 )
{
g_bRunning = FALSE;
CString str;
str.Format("read error!%d", temp);
AfxMessageBox(str);
break;
}
Sleep(1);
}
return 0;
}
void CTestMultiyAccessIntDlg::OnButton1()
{
g_bRunning = TRUE;
for ( int i = 0; i < 50; i++ )
{
//创建50个写线程1
CreateThread( NULL, 0, thWriteProc1, NULL, 0, NULL );
}
for ( int i = 0; i < 50; i++ )
{
//创建50个写线程2
CreateThread( NULL, 0, thWriteProc2, NULL, 0, NULL );
}
for ( int i = 0; i < 50; i++ )
{
//创建50个读线程
CreateThread( NULL, 0, thReadProc, NULL, 0, NULL );
}
}


测试方法:
改变g_test的类型,给g_test赋予不同的值(不要超过类型的上限值)

测试现象:
当g_test为int,short char时,不存在多线程交叉读写错误的问题
当g_test为double, float, __int64时,存在多线程交叉读写错误的问题,对于__int64,当赋值小于0xFFFFFFFF时不出错,当大于0xFFFFFFFF时出错
当g_test为CString时,存在交叉读写错误,有时候程序崩溃
另:不加Sleep(1)机器卡死过,CPU占用率达到100%,4个核心占用率全满,可以保证运行在多核环境下

现象分析:
(1)int short char均为小于4字节的连续内存块,CPU一条指令就可以读写它们的值,CPU不可能同一个时间执行两条指令
(2)double为8字节,如果写线程先写了4字节,读线程读了8字节,这自然导致数据被破坏
(3)float也为4字节,我也不是太清楚为什么不行,可能是VC对浮点数的处理比较特殊有关,浮点数具有复杂内存结构
(4)__int64为8字节,存在和(2)相同的情况,如果__int64小于等于0xFFFFFFFF,相当于只改变了低4字节,因此就没有问题
(5)CString为类类型,具有复杂结构,显然不行

结论:
1.对于int,short,char,BOOL等小于等于4字节的简单数据类型,如果无逻辑上的先后关系,多线程读写可以完全不用加锁
2.尽管float为4字节,多线程访问时也需要加锁
3.对于大于4字节的简单类型,比如double,__int64等,多线程读写必须加锁。
4.对于所有复杂类型,比如类,结构体,容器等类型必须加锁

尽管对int等类型的多线程读写不需要加锁,但是逻辑上有必要加锁的还是应该加锁
例如:对于一个多线程访问的全局变量int g_test
int count = g_test/1024;
int mod = g_test%1024;
由于是两条语句,执行完第一条之后,别的线程很可能已经修改了g_test的值,如果希望这两条语句执行时,g_test不发生变化,就必须加锁,以保证两条语句执行的整体性。
Lock();
int count = g_test/1024;
int mod= g_test%1024;
UnLock();
如果不加锁,也可以改为先保存到一个临时变量里
int temp = g_test;
int count = temp/1024;
int mod = temp%1024;
...全文
16848 3 收藏 176
写回复
176 条回复
切换为时间正序
当前发帖距今超过3年,不再开放新的回复
发表回复
l252114997 2013-08-07
感谢楼主,我纠结这个问题很久了。 虽然做的不是很底层的项目,但是利用这种特殊情况不加密的特性,确实很有用。 因为我只需要在一个线程里读bool值,另外一个线程去写bool值。 读线程会读多个bool变量,但每个bool各有一个线程进行写操作。 这种情况不加锁明显提高效率,而且免去加锁代码的麻烦。
回复
JINGRH 2011-11-02
如果要加锁的话,我在很多地方用了变量怎么办,而且是只读的。写操作在一个专门的线程里。
回复
dreamzme 2011-09-15
学习了。
回复
zhangyanfei01 2011-04-18
[Quote=引用 171 楼 arofree 的回复:]
115楼所说的问题属于第一类

SMP 架构,多cpu,使用一条地址总线,所以,一个cpu访问内存时,其他不能访问的
32位CPU,访问内存一次肯定可以读写32位,不会出现一个变量前后半边不一致

numa 架构,多cpu,各自有数据总线,这个就不清楚,怎么访问情况了,有知道的说一下
[/Quote]

这位给出了冰山的另一角, 很赞!
回复
stone87439312 2011-04-15
必须支持哦
回复
莫名的默默 2011-02-28
115楼所说的问题属于第一类

SMP 架构,多cpu,使用一条地址总线,所以,一个cpu访问内存时,其他不能访问的
32位CPU,访问内存一次肯定可以读写32位,不会出现一个变量前后半边不一致

numa 架构,多cpu,各自有数据总线,这个就不清楚,怎么访问情况了,有知道的说一下
回复
莫名的默默 2011-02-28
[Quote=引用 115 楼 pitchstar 的回复:]
很多人都没有明白lz在说什么,其实你们说的这些都是基础知识,大家都知道.lz说的不加锁有特定的场景,举例如下:
i是个全局变量
第一个线程执行 i = 0xffff0000;
第二个线程执行 i = 0x0000ffff;

那么,你在任何时候用第3个线程去读 i,它的值是不是一定是这2者之一?还是有可能会读到 0xffffffff 或者 0,甚至 0xff00ff00?

lz通过……
[/Quote]
真给力,这个问题,查了查资料,
多线程数据访问,是否加锁问题,
我认为这个问题分为两部分

一、某个内存单元,是否可以被多个CPU同时访问
二、内存数据访问,凡是把数据得读到寄存器里的指令,会引起,寄存器和内存不同步,此时,最大的问题是执行次序的问题,单核CPU也会出现
回复
莫名的默默 2011-02-28
[Quote=引用 80 楼 qustdong 的回复:]
引用 5 楼 happyparrot 的回复:

引用 2 楼 qustdong 的回复:
如果我没有弄错的话,float应该也是8字节的。

谢谢楼主对于这个问题的深入研究,学习了~~~

float是8字节的话,那double是几个字节?


我肤浅了,谢谢楼主纠错~~~~
[/Quote]
float a = 1;
double b = 2;
printf("%f %f",a, b);
明显a,b长度不同,为什么用同一个%f,求高手
回复
robin41209 2010-12-24
楼主果然有心。
回复
ACDINO 2010-12-24
[Quote=引用 71 楼 rebort_q 的回复:]
要的就是这种精神!
[/Quote]
+
回复
yourname2002 2010-12-23
对同一个变量在不同进程赋值应该要用互斥吧。
回复
hotedc 2010-12-23
收藏,多谢楼主分享
回复
holymoon858 2010-12-22
建议大家还是多看看MSDN,写得很清楚

Interlocked Variable Access

Applications must synchronize access to variables that are shared by multiple threads. Applications must also ensure that operations on these variables are performed atomically (performed in their entirety or not at all.)

Simple reads and writes to properly-aligned 32-bit variables are atomic operations. In other words, you will not end up with only one portion of the variable updated; all bits are updated in an atomic fashion. However, access is not guaranteed to be synchronized. If two threads are reading and writing from the same variable, you cannot determine if one thread will perform its read operation before the other performs its write operation.

Simple reads and writes to properly aligned 64-bit variables are atomic on 64-bit Windows. Reads and writes to 64-bit values are not guaranteed to be atomic on 32-bit Windows. Reads and writes to variables of other sizes are not guaranteed to be atomic on any platform.
回复
beaugauge2011 2010-12-22
还是都加锁的好

--------------------------
精美虚拟仪表控件
www.beaugauge.com
回复
jeam0402 2010-12-22
看来有些问题还是要讨论了才明白啊
回复
nixiaofeng 2010-12-22
学习了,不过大部分情况下判断是否要加锁都是逻辑问题
回复
上岸の鱼 2010-12-22
[Quote=引用 72 楼 jameshooo 的回复:]
伪命题,无讨论必要。

因为不可能一个指令完成对一个整数的+1或-1操作,如果不加锁,大量存在的对引用计数值非常敏感的PV类操作不可能正确完成,所谓的 i++ 操作一定是分成“读取”、“运算”、“写入”三个步骤,读取的时候正确,但写入时已经错了,除非CPU直接提供单一的+1/-1指令。这个跟整数占几个字节毫无关系。

a = 12345; 这种常数赋值就一个指令,当然不会错,实际应用中几乎……
[/Quote]
这位大佬讲的也很有道理
回复
上岸の鱼 2010-12-22
[Quote=引用楼主 jackson35296 的回复:]
对于多线程访问同一变量是否需要加锁的问题,先前大家都讨论过。今天用代码验证了一下之前的猜想:32位CPU与内存的最小交换数据为4字节/次,这也是结构体要对齐4字节的原因。在物理上,CPU对于同一4字节的内存单元,不可能写2个字节的同时,又读了3字节。
[/Quote]

很仔细地看完楼主的文章,非常感谢他这么认真地研究并能与我们分享。
但我有点不明白,结构体为什么要对齐4字节,这是什么意思?还有最后一句,4字节的内存单元,写了2字节,我读这个内存单元跟写有什么联系呢?希望有人能帮我回答一下
回复
攻城狮·正 2010-12-22
还是加锁安全
回复
curpage 2010-12-22
我也觉得需要加锁的飘过!
回复
加载更多回复
相关推荐
发帖
进程/线程/DLL
创建于2007-09-28

1.5w+

社区成员

VC/MFC 进程/线程/DLL
申请成为版主
帖子事件
创建了帖子
2010-12-17 02:22
社区公告
暂无公告