欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

多线程 变量 加锁问题

程序员文章站 2022-03-23 23:21:27
...
对于x86来说如果其地址是4字节对齐的,那访问是就原子操作。这也是最常见的情况,注意访问是读或写,不是自增等。
如果你有个这样的结构体,还禁用了对齐的话那就不是了。
struct S
{
	char a;
	int b;

};

http://www.xuebuyuan.com/2214900.html

对于多线程编程,很多人概念不清,写代码的时候要么是处处加锁,影响性能不说,还容易莫名其妙的死锁,还有人对多线程敬而远之。

所以学习多线程编程最重要的不是学习API,而是理解什么才是多线程安全的代码

从例子说起

#include <windows.h> #include <process.h>

long global1 = 0; volatile long global2 = 0;

class MyClass { public:     MyClass() : m(0)     {         ++m;     }

    int fun(int v)     {         return m+v; //-----------9     }

    void set(int v)     {         m = v;   //-------------10     }     int m; };

MyClass global_object; //-------------8 unsigned int __stdcall thread_fun1(void *param) {     static int static2 = 0;     static MyClass static_object; //--------6     int local1 = 0;          ++local1;     //-------1     ++static2;    //-------2     ++global1;    //-------3     ++global2;    //-------4     InterlockedIncrement(&global1); //--------5     local1 = global_object.fun(local1); //----------7     global_object.set(local1); //---------------11     return 0; }

unsigned int __stdcall thread_fun2(void *param) {     ++global1;    //-------3     ++global2;    //-------4     InterlockedIncrement(&global1); //--------5     global_object.set(1); //-----------11     return 0; }

int main() {     HANDLE thread1 = (HANDLE)_beginthreadex(0,0,&thread_fun1,0,0,0); //thread 1     HANDLE thread2 = (HANDLE)_beginthreadex(0,0,&thread_fun1,0,0,0); //thread 2     HANDLE thread3 = (HANDLE)_beginthreadex(0,0,&thread_fun2,0,0,0); //thread 3          WaitForSingleObject(thread1,INFINITE);     WaitForSingleObject(thread2,INFINITE);     WaitForSingleObject(thread3,INFINITE);          return 0; }

1.局部变量局部使用是安全的 为什么?因为每个thread 都有自己的运行堆栈,而局部变量是生存在堆栈中,大家不干扰。 所以代码1 int local1; ++local1; 是安全的

2.全局原生变量多线程读写是不安全的 全局变量是在堆(heap)中 long global1 = 0; ++global2; ++这个操作其实分为两部,一个是读,另外一个是写  mov         ecx,global  add         ecx,1  mov         global,ecx 所以代码3处是不安全的

3.函数静态变量多线程读写也是不安全的 道理同2 所以代码2处也是不安全的

4.volatile能保证全局整形变量是多线程安全的么 不能。 volatile仅仅是告诫compiler不要对这个变量作优化,每次都要从memory取数值,而不是从register 所以代码4也不是安全

5.InterlockedIncrement保证整型变量自增的原子性 所以代码5是安全的

6.function static object的初始化是多线程安全的么 不是。 著名的Meyer Singleton其实不是线程安全的 Object & getInstance() {       static Object o;      return o; } 可能会造成多次初始化对象 所以代码6处是不安全的

7.在32机器上,4字节整形一次assign是原子的 比如 i =10; //thread1 i=4; //thread2 不会导致i的值处于未知状态,要么是10要么是4

其它的大家自己去体会。

写好多线程安全的法宝就是封装,使数据有保护的被访问到 安全性: 局部变量>成员变量>全局变量

http://www.phpfans.net/ask/MTMxODE4OQ.html

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

对于多线程访问同一变量是否需要加锁的问题,先前大家都讨论过。今天用代码验证了一下之前的猜想:32位CPU与内存的最小交换数据为4字节/次,这也是结构体要对齐4字节的原因。在物理上,CPU对于同一4字节的内存单元,不可能写2个字节的同时,又读了3字节。 测试环境为: XEON 2CPU*2 Windows7 采用50,50,50线程交叉读写,试验代码如下:
C/C++ code

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;
昵称: jackson35296  时间: 2010-12-17 14:22:10
顶。需要楼主这样用心的人,给大家提供实实在在的帮助。
我没有细看,但估计对我的总结来说就是,加锁吧......多线程 变量 加锁问题
昵称: happyparrot  时间: 2010-12-17 14:25:58
如果我没有弄错的话,float应该也是8字节的。

谢谢楼主对于这个问题的深入研究,学习了~~~
昵称: QustDong  时间: 2010-12-17 14:58:44
‎‎‎‎‎‎
昵称: linwengk  时间: 2010-12-17 15:02:07
恩,不错,呵呵~~~谢谢哦~~~


0

+=,-=++,--需要。

x=value;

value=x;

不需要加锁。

0