在多线程中操作全局变量一般都会引起线程冲突,为了解决线程冲突,引入原子操作。
1.线程冲突
#include #include #include #include int g_count = 0;void count(void *p){Sleep(100); //do some work//每个线程把g_count加1共10次for (int i = 0; i < 10; i++){g_count++;}Sleep(100); //do some work}int main(void){printf("******多线程访问全局变量演示***by David***");//共创建10个线程HANDLE handles[10];for (int i = 0; i < 10; i++){for (int j = 0; j < 10; j++){handles[j] = _beginthread(count, 0, NULL);}WaitForMultipleObjects(10, handles, 1, INFINITE);printf("%d time g_count = %d", i, g_count);//重置g_count = 0;}getchar();return 0;}
运行
理论上,g_count的最后结果应该是100,可事实却并非如此,不但结果不一定是100,而且每次的结果还很可能不一样。原因是,多个线程对同一个全局变量进行访问时,特别是在进行修改操作,会引起冲突。详细解释:
设置断点,查看反汇编
g_count++;被分为三步操作:
1.把g_count的内容从内存中移动到寄存器eax
2.把寄存器eax加1
3.把寄存器eax中的内容移动到内存g_count的地址
三步以后,g_count的值被顺利加1。
cpu执行的时间片内包含多条指令,每条指令执行期间不会被打断,但如果一个操作包含多条指令,则很有可能该操作会被打断。g_count++;就是这样的操作。
g_count被顺利加到100的前提:每次加1,都建立在上一次加1顺利完成的基础上。也就是说,如果上一次加1被打断,这一次的加1就得不到上一次加1的累积效果。自然,最后的结果,多半会小于100。
2.原子操作
所谓原子操作,是指不会被线程调度机制打断的操作,操作一旦开始,就得执行到结束为止。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。原子操作一般靠底层汇编实现。
在头文件winnt.h中提供了很多的原子操作函数,它们使用自加锁的方式,保证操作的原子性,如自增操作
InterlockedIncrement,
函数原型
LONG CDECL_NON_WVMPURE InterlockedIncrement(
_Inout_ _Interlocked_operand_ LONG volatile *Addend
);
其它相关操作,请自行查看头文件。
使用该函数,我们修改线程函数count。修改很简单,只需把g_count++改为InterlockedIncrement((LONG)&g_count);即可。
运行如下
显然,在原子操作下,我们肯定是可以得到正确结果的。
需要C/C++ Linux服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享