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

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;
...全文
18090 176 打赏 收藏 转发到动态 举报
写回复
用AI写文章
176 条回复
切换为时间正序
请发表友善的回复…
发表回复
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
  • 打赏
  • 举报
回复
我也觉得需要加锁的飘过!
加载更多回复(114)
同步概念 所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等 而,编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。 线程同步 同步即协同步调,按预定的先后次序运行。 线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。 举例1: 银行存款 5000。柜台,折:取3000;提款机,卡:取 3000。剩余:2000 举例2: 内存中100字节,线程T1欲填入全1, 线程T2欲填入全0。但如果T1执行了50个字节失去cpu,T2执行,会将T1写过的内容覆盖。当T1再次获得cpu继续 从失去cpu的位置向后写入1,当执行结束,内存中的100字节,既不是全1,也不是全0。 产生的现象叫做“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。 “同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。 因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步。 数据混乱原因: 1. 资源共享(独享资源则不会) 2. 调度随机(意味着数据访问会出现竞争) 3. 线程间缺乏必要的同步机制。 以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。 所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。 互斥量mutex Linux中提供一把互斥锁mutex(也称之为互斥量)。 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。 资源还是共享的,线程间也还是竞争的, 但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。 但,应注意:同一时刻,只能有一个线程持有该锁。 当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。 所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。 因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。 主要应用函数: pthread_mutex_init函数 pthread_mutex_destroy函数 pthread_mutex_lock函数 pthread_mutex_trylock函数 pthread_mutex_unlock函数 以上5个函数的返回值都是:成功返回0, 失败返回错误号。 pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。 pthread_mutex_t mutex; 变量mutex只有两种取值1、0。 pthread_mutex_init函数 初始化一个互斥锁(互斥量) ---> 初值可看作1 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 参1:传出参数,调用时应传 &mutex restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改 参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。 参APUE.12.4同步属性 1. 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化。e.g. pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER; 2. 动态初始化:局部变量应采用动态初始化。e.g. pthread_mutex_init(&mutex, NULL) pthread_mutex_destroy函数 销毁一个互斥锁 int pthread_mutex_destroy(pthread_mutex_t *mutex); pthread_mutex_lock函数 加锁。可理解为将mutex--(或-1) int pthread_mutex_lock(pthread_mutex_t *mutex); pthread_mutex_unlock函数 解锁。可理解为将mutex ++(或+1) int pthread_mutex_unlock(pthread_mutex_t *mutex); pthread_mutex_trylock函数 尝试加锁 int pthread_mutex_trylock(pthread_mutex_t *mutex); 加锁与解锁 lock与unlock: lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。 unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。 例如:T1 T2 T3 T4 使用一把mutex锁。T1加锁成功,其他线程均阻塞,直至T1解锁。T1解锁后,T2 T3 T4均被唤醒,并自动再次尝试加锁。 可假想mutex锁 init成功初值为1。 lock 功能是将mutex--。 unlock将mutex++ lock与trylock: lock加锁失败会阻塞,等待锁释放。 trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。 加锁步骤测试: 看如下程序:该程序是非常典型的,由于共享、竞争而没有加任何同步机制,导致产生于时间有关的错误,造成数据混乱: #include #include #include void *tfn(void *arg) { srand(time(NULL)); while (1) { printf("hello "); sleep(rand() % 3); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/ printf("world\n"); sleep(rand() % 3); } return NULL; } int main(void) { pthread_t tid; srand(time(NULL)); pthread_create(&tid, NULL, tfn, NULL); while (1) { printf("HELLO "); sleep(rand() % 3); printf("WORLD\n"); sleep(rand() % 3); } pthread_join(tid, NULL); return 0; } 【mutex.c】 【练习】:修改该程序,使用mutex互斥锁进行同步。 1. 定义全局互斥量,初始化init(&m, NULL)互斥量,添加对应的destry 2. 两个线程while中,两次printf前后,分别加lock和unlock 3. 将unlock挪至第二个sleep后,发现交替现象很难出现。 线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁,这两个库函数本身不会阻塞。 所以在这两行代码之间失去cpu的概率很小。因此,另外一个线程很难得到加锁的机会。 4. main 中加flag = 5 将flg在while中-- 这时,主线程输出5次后试图销毁锁,但子线程未将锁释放,无法完成。 5. main 中加pthread_cancel()将子线程取消。 【pthrd_mutex.c】 结论: 在访问共享资源前加锁访问结束后立即解锁。锁的“粒度”应越小越好。 死锁 1. 线程试图对同一个互斥量A加锁两次。 2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁 【作业】:编写程序,实现上述两种死锁现象。 读写锁 与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。 读写锁状态: 一把读写锁具备三种状态: 1. 读模式下加锁状态 (读锁) 2. 写模式下加锁状态 (写锁) 3. 不加锁状态 读写锁特性: 1. 读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。 2. 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。 3. 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高 读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。 读写锁非常适合于对数据结构读的次数远大于写的情况。 主要应用函数: pthread_rwlock_init函数 pthread_rwlock_destroy函数 pthread_rwlock_rdlock函数 pthread_rwlock_wrlock函数 pthread_rwlock_tryrdlock函数 pthread_rwlock_trywrlock函数 pthread_rwlock_unlock函数 以上7 个函数的返回值都是:成功返回0, 失败直接返回错误号。 pthread_rwlock_t类型 用于定义一个读写锁变量。 pthread_rwlock_t rwlock; pthread_rwlock_init函数 初始化一把读写锁 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); 参2:attr表读写锁属性,通常使用默认属性,传NULL即可。 pthread_rwlock_destroy函数 销毁一把读写锁 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); pthread_rwlock_rdlock函数 以读方式请求读写锁。(常简称为:请求读锁) int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); pthread_rwlock_wrlock函数 以写方式请求读写锁。(常简称为:请求写锁) int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); pthread_rwlock_unlock函数 解锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); pthread_rwlock_tryrdlock函数 非阻塞以读方式请求读写锁(非阻塞请求读锁) int pthread_

15,472

社区成员

发帖
与我相关
我的任务
社区描述
VC/MFC 进程/线程/DLL
社区管理员
  • 进程/线程/DLL社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

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