多线程 变量 加锁问题

C++ 专栏收录该内容
13 篇文章 0 订阅
对于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

  • 1
    点赞
  • 0
    评论
  • 5
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值