C语言/原子/编译,你真的明白了吗?
版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖。如要转贴,必须注明原文网址 http://www.cnblogs.com/Colin-Cai/p/7668982.html 作者:窗户 QQ:6679072 E-mail:6679072@qq.com
说到原子,类似于以下的代码可能人人都可以看出猫腻。
/* http://www.cnblogs.com/Colin-Cai */
#include <stdio.h>
#include <pthread.h> int cnt = ;
void* mythread(void* arg)
{
int i;
for(i=;i<;i++)
cnt++;
return NULL;
} int main()
{
pthread_t id, id2; pthread_create(&id, NULL, mythread, NULL);
pthread_create(&id2, NULL, mythread, NULL);
pthread_join(id, NULL);
pthread_join(id2, NULL);
printf("cnt = %d\n", cnt); return ;
}
我想大多数人都知道其结果未必会得到1000000000。
测试一下吧。
linux-p94b:/tmp/testhere # gcc test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 958925625
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
可是真的知道猫腻了吗?如果我编译的时候优化一下呢?
linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
运行速度一下子变的飞快,而且似乎都得到了10亿。
这里,mythread里cnt自加5亿次被优化成了 cnt += 500000000
那么当然快啊,可是似乎这与我们当初想测试原子有那么一些差异,一样的代码,不一样的编译,却带来了不同的结果。
其实原因在于,我们这里代码写的不好,才没有表达好我们当初的意思,我们是希望cnt真的自加5亿次。那么怎么办呢?其实很好办,在cnt的定义前面加个volatile,那么这里对于cnt的自加则不会优化。很多时候,为什么我们优化前和优化后的结果不一样,常常是因为写代码的人不明白程序的优化规则。在上个公司的时候,我很想临走的时候再给大家做一个培训,说说C语言的优化,同时说说我们平时写的无意依赖于编译的所谓垃圾代码,但是直到离开,我还是没有做此培训。
我们加了volatile试一下,
linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 635981117
cnt = 675792826
cnt = 522700646
cnt = 593410055
cnt = 544306380
cnt = 630888304
cnt = 580539893
cnt = 629360072
cnt = 555570127
我们在cnt定义前加个volatile,效果果然就更明显了,因为真的是自加5亿次,导致问题的机会变多了。那么之前没加volatile并优化编译,会不会也有不得到10亿的可能呢?
我们首先要明白的是,这里的cnt++不是原子操作,中间有随时调度的可能。
5亿次太多,我们就拿只自加1次为例即可说明,两个线程都只自加1次,本来期待结果为2.
cnt++在一般的处理器中至少有三条指令,我们用伪汇编来写。
cnt -> reg //把cnt从内存加载到寄存器reg
reg+1 -> reg //寄存器reg自加1
reg -> cnt //把reg的内容写入内存
那么,
(线程1)cnt -> reg
(线程1)reg+1 -> reg
(线程1)reg -> cnt
(线程2)cnt -> reg
(线程2)reg+1 -> reg
(线程2)reg -> cnt
理想中,我们认为处理器的执行是以上这样,结果cnt里的值是2。
但假设过程中发生了调度,指令执行的顺序并非像以上这样,假如变成了以下这样
(线程1)cnt -> reg
(线程1)reg+1 -> reg
(线程2)cnt -> reg
(线程2)reg+1 -> reg
(线程2)reg -> cnt
(线程1)reg -> cnt
我们再来算算,
cnt = 0, reg任意
(线程1)cnt -> reg
cnt = 0, reg = 0
(线程1)reg+1 -> reg
cnt = 0, reg = 1
此处调度,reg = 1会被保存,并在重新调度回来之后有效,而cnt不会管
调度之后
cnt = 0, reg任意
(线程2)cnt -> reg
cnt = 0, reg = 0
(线程2)reg+1 -> reg
cnt = 0, reg = 1
(线程2)reg -> cnt
cnt = 1, reg = 1
此处又发生调度,reg会恢复之前保存的1,而cnt不会有任何变化
所以在执行下一条指令前,
cnt = 1, reg = 1
(线程1)reg -> cnt
cnt = 1, reg = 1
我们可以看到,结果成了1,而不是2,这就是非原子操作导致的结果,其实之前优化成cnt += 500000000本身也依然有此问题,只是难以观察的到。
虽然x++不是原子,但是我们可以使用锁的方式,来人为的制造“原子”,比如这里用互斥。
#include <stdio.h>
#include <pthread.h> volatile int cnt = ;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* mythread(void* arg)
{
int i;
for(i=;i<;i++) {
pthread_mutex_lock(&mutex);
cnt++;
pthread_mutex_unlock(&mutex);
}
return NULL;
} int main()
{
pthread_t id, id2; pthread_create(&id, NULL, mythread, NULL);
pthread_create(&id2, NULL, mythread, NULL);
pthread_join(id, NULL);
pthread_join(id2, NULL);
printf("cnt = %d\n", cnt); return ;
}
测试一下
linux-p94b:/tmp/testhere # gcc -O2 test1.c -lpthread
linux-p94b:/tmp/testhere # for((i=0;i<10;i++));do ./a.out ; done
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
cnt = 1000000000
C语言/原子/编译,你真的明白了吗?的更多相关文章
- 我学C的那些年[ch01]:浅淡C语言的编译过程
前几天大致学习了C语言的编译过程,那么今天就和大家分享一下 首先,编译C语言,需要一个文本编辑器(windows自带的也行),和一个MinGW编译器(需要配置环境),就可以将.c文件编译成.exe文件 ...
- python变量、对象和引用你真的明白了吗
python变量.对象和引用你真的明白了吗 变量.对象和引用 Python不像C++,Java等语言一样,他们可以不用事先声明变量类型而直接对变量进行赋值.对Python语言来讲,对象的类型和内存都是 ...
- C语言模块化编译介绍
C语言模块化编译介绍 模块化编程的概念 所谓模块化变成(多文件开发),就是多文件(.c文件)编程,一个.c文件和一个.h文件可以被称为一个模块. 头文件开发的注意事项: 1)头文件中可以和C程序一样引 ...
- C语言的编译过程、安装gcc编译器以及设置环境变量
以我对C语言编译过程的了解,我用了一点时间画了一个图,提供给大家参考一下,希望有些能对您的问题提上帮助. 前几天刚初步学习了C语言的编译过程,感触挺深的.在C语言中头文件其实起了一个很大的作用. 1. ...
- Koala – 开源的前端预处理器语言图形编译工具
koala 是一个前端预处理器语言图形编译工具,支持 Less.Sass.Compass.CoffeeScript,帮助 Web 开发者更高效地使用它们进行开发.跨平台运行,完美兼容 Windows. ...
- 李洪强漫谈iOS开发[C语言-004]-开发概述程序设计语言程序编译过程
汇编语言 指令用特定的名字来标记,这就是汇编语言 人比较容易看懂汇编语言 汇编直接和程序一一对应的 有汇编器把程序翻译成机器码 把高级语言编译成计算机识别的语言 程序编译过程 命令行 UNIX 系统中 ...
- C语言的编译过程和GCC编译参数
C语言的编译一般有三个步骤: 预编译: gcc -E -o a.e a.c 预编译a.c文件,生成的目标文件名为a.e 预编译就是将include包含的头文件内容替换到C文件中,同时删除代码中没用的注 ...
- c语言的编译过程和GCC 编译参数
原文: http://www.cnblogs.com/zhangShanGui/p/4912135.html C语言的编译过程和GCC编译参数 C语言的编译一般有三个步骤: 预编译: gcc -E - ...
- .NET语言的编译过程:中间语言(IL)和即时编译器(JIT)
.NET语言的编译分为两个阶段.首先高级语言被编译成一种称作IL的中间语言,与高级语言相比,IL更像是机器语言,然而,IL却包含一些抽象概念(比如:类.异常),这也是这种语言被称为中间语言的原因.IL ...
随机推荐
- 10 Logistic Regression
线性分类中的是非题 --->概率题 (设置概率阈值后,大于等于该值的为O,小于改值的为X) --->逻辑回归 O为1,X为0 逻辑回归假设 逻辑函数/S型函数:光滑,单调 自变量趋于负无穷 ...
- Linux下undefined reference to ‘pthread_create’问题解决
Linux下undefined reference to 'pthread_create'问题解决 在试用Linux 线程模块时,试用pthread_create 函数. 编译命令为 gcc main ...
- 软工+C(2017第7期) 野生程序员
// 上一篇:最近发展区/脚手架 // 下一篇:提问和回复 怎样做足够好的软件?我们就差一个程序员! 没有什么软件工程的理论的时候,程序员们凭借自己对编程的热爱,凭借着:"这是一个可以自动化 ...
- Git和Github使用
什么是Git? Git 是一个快速.可扩展的分布式版本控制系统,它具有极为丰富的命令集,对内部系统提供了高级操作和完全访问. 版本控制 简单地说,就是将在本地开发的代码,定时推送到服务器.每一次修改, ...
- 团队作业4——第一次项目冲刺(Alpha版本)7th day
一.Daily Scrum Meeting照片 二.燃尽图 三.项目进展 在计时模式下能够记录用户的用户名和成绩,没有弄登录功能, 将程序定义为单机的 未完成的卡片为登录功能和使用QQ登录. 四.困难 ...
- 201521123102 《Java程序设计》第6周学习总结
1. 本周学习总结 2. 书面作业 Q1.clone方法 1.1 Object对象中的clone方法是被protected修饰,在自定义的类中覆盖clone方法时需要注意什么? 子类要实现Clonea ...
- 使用Eclipse Egit与码云管理你的代码
总体流程: 建立远程仓库 建立本地仓库并与远程仓库关联 将Eclipse中的项目提交到本地仓库并进而push到远程仓库 一. 配置Eclipse EGit 图解Eclipse中安装及配置EGit插件中 ...
- 201521123062《Java程序设计》第11周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容. 2. 书面作业 1.互斥访问与同步访问 完成题集4-4(互斥访问)与4-5(同步访问) 1.1 除了使用synch ...
- JAVA课程设计--简易计算器(201521123022 黄俊麟)
1.团队课程设计博客链接 http://www.cnblogs.com/I-love-java/p/7058752.html 2.个人负责模板或任务说明 1.初始化业务逻辑. 2.开方.正负.清零.退 ...
- 201521123027 <java程序设计>第11周学习总结
1.本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容. 2.书面作业 1.互斥访问与同步访问 完成题集4-4(互斥访问)与4-5(同步访问) 1.1 除了使用synchro ...