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 ...
随机推荐
- Markdown(editormd)语法解析成html
我们在一些网站中可以见到一款网页编辑器--markdown: 这是一款功能强大的富文本编辑器,之前自己在网页上使用的时候遇到了一点点的问题,现在跟大家分享下 在我们写了文章之后是需要将内容保存到数据库 ...
- 【集美大学1411_助教博客】团队作业7——Alpha冲刺之事后诸葛亮
写在前面的话 alpha阶段都顺利完成了,大家这次作业完成得都很认真.我觉得通过这些问题,大家既可以回顾自己的alpha阶段,又可以给beta阶段做一些指引.但看了所有组的博客,没有一个组在这些问题之 ...
- 团队作业8——第二次项目冲刺(Beta阶段)第二天
BETA阶段冲刺第二天 1.当天站立式会议 2.每个人的工作 (1) 昨天已完成的工作: Alpha阶段的Bug修复 (2) 今天计划完成的工作: 编写前端页面 (3) 工作中遇到的困难: 对于前端页 ...
- 团队作业8 ----第二次项目冲刺(Beta阶段)博客汇总
一.冲刺计划安排 团队作业8--Beta版本冲刺计划及安排 二.七天冲刺汇总 [Beta]第一次Daily Scrum Meeting [Beta]第二次Daily Scrum Meeting [Be ...
- 201521123023《Java程序设计》第8周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 1.2 选做:收集你认为有用的代码片段 2. 书面作业 本次作业题集集合 1.List中指定元素的删除(题目4 ...
- 201521123112《Java程序设计》第14周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多数据库相关内容. 关系型数据库系统:使用表来存储数据,使用行来区分不同记录. 主键可以唯一确定一条记录. 常见的数据库管理系统有: ...
- 201521123014 《Java程序设计》第9周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常相关内容. 异常(Exception)处理 异常(Exception)的概念:在程序运行的时候可能出错,Java中把程序出现的错 ...
- hdu3037 Saving Beans
Saving Beans Time Limit: 6000/3000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Pro ...
- 第三节课:简单的网络命令和ARP欺骗
MTU :最大传输单元 RX:收包 TX: 发包 Ifconfig: IP配置命令,config是linux中用于显示或配置网络设备(网络接口卡)的命令 ifconfig eth0 192.168 ...
- SpringMVC第七篇【RESTful支持、拦截器】
RESTful支持 我们在学习webservice的时候可能就听过RESTful这么一个名词,当时候与SOAP进行对比的-那么RESTful究竟是什么东东呢??? RESTful(Representa ...