__sync_fetch_and_add
最近在公司离职的前辈写的代码哪里看到了__sync_fetch_and_add这个东东.比较好奇.找些资料学习学习
http://www.lxway.com/4091061956.htm
http://www.cnblogs.com/FrankTan/archive/2010/12/11/1903377.html
可使用的环境: gcc.version > 4.1.2
作用:提供多线程下变量的加减和逻辑运算的原子操作
正文如下:
最近编码需要实现多线程环境下的计数器操作,统计相关事件的次数。下面是一些学习心得和体会。不敢妄称原创,基本是学习笔记。遇到相关的引用,我会致谢。
当然我们知道,count++这种操作不是原子的。一个自加操作,本质是分成三步的:
1 从缓存取到寄存器
2 在寄存器加1
3 存入缓存。
由于时序的因素,多个线程操作同一个全局变量,会出现问题。这也是并发编程的难点。在目前多核条件下,这种困境会越来越彰显出来。
最简单的处理办法就是加锁保护,这也是我最初的解决方案。看下面的代码:
pthread_mutex_t count_lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&count_lock);
global_int++;
pthread_mutex_unlock(&count_lock);
linux 变量 : pthread_mutex_t
linux 函数 : pthread_mutex_lock; pthread_mutex_unlock
后来在网上查找资料,找到了__sync_fetch_and_add系列的命令
__sync_fetch_and_add系列一共有十二个函数,有加/减/与/或/异或/等函数的原子性操作函数,
__snyc_fetch_and_add : 先fetch然后自加,返回的是自加以前的值
__snyc_add_and_fetch : 先自加然后返回,返回的是自加以后的值 (参照 ++i 和 i++) __snyc_fetch_and_add的一个简单使用
1 int count = ;
__sync_fetch_and_add(&count, ); // __sync_fetch_and_add(&count, 1) == 4
cout<<count<<endl; //--->count=5
对于多线程对全局变量进行自加,我们就再也不用理线程锁了。
下面这行代码,和上面被pthread_mutex保护的那行代码作用是一样的,而且也是线程安全的。
__sync_fetch_and_add( &global_int, );
下面是这群函数的全家福,大家看名字就知道是这些函数是干啥的了。
//在用gcc编译的时候要加上选项 -march=i686
type __sync_fetch_and_add (type *ptr, type value, ...);
type __sync_fetch_and_sub (type *ptr, type value, ...);
type __sync_fetch_and_or (type *ptr, type value, ...);
type __sync_fetch_and_and (type *ptr, type value, ...);
type __sync_fetch_and_xor (type *ptr, type value, ...);
type __sync_fetch_and_nand (type *ptr, type value, ...);
type __sync_add_and_fetch (type *ptr, type value, ...);
type __sync_sub_and_fetch (type *ptr, type value, ...);
type __sync_or_and_fetch (type *ptr, type value, ...);
type __sync_and_and_fetch (type *ptr, type value, ...);
type __sync_xor_and_fetch (type *ptr, type value, ...);
type __sync_nand_and_fetch (type *ptr, type value, ...);
__sync_fetch_and_add,速度是线程锁的6~7倍
type可以是1,2,3或者8字节长度的int类型,即
int8_t
uint8_t int16_t
uint16_t int32_t
uint32_t int64_t
uint64_t
后面的可扩展参数(...)用来指出哪些变量需要memory barrier,因为目前gcc实现的是full barrier(类似于linux kernel 中的mb(),表示这个操作之前的所有内存操作不会被重排序到这个操作之后),所以可以略掉这个参数。
恩.再找个帖子学习学习.http://blog.csdn.net/hzhsan/article/details/25124901
有一个概念叫过无锁化编程, 知道linux支持的哪些操作是具有原子特性的是理解和设计无锁化编程算法的基础
除了上面提到的12个外 还有4个可以实现互斥锁的功能
//以下两个函数提供原子的比较和交换, 如果*ptr = oldValue, 就将newValue写入*ptr
//第一个函数在相等并写入的情况下返回true
//第二个函数返回操作之前的值 bool __sync_bool_compare_and_swap(type* ptr, type oldValue, type newValue, ....); type __sync_val_compare_and_swap(type* ptr, type oldValue, type newValue, ....); //将*ptr设为value并返回*ptr操作之前的值
type __sync_lock_test_and_set(type *ptr, type value, ....); //置*ptr为0
void __sync_lock_release(type* ptr, ....);
__sync_synchronize(...) //作用 : 发出一个full barrier
/*关于memory barrier,cpu会对我们的指令进行排序,一般说来会提高程序的效率,但有时候可能造成我们不希望得到的结果,举一个例子,比如我们有一个硬件设备,它有4个寄存器,当你发出一个操作指令的时候,一个寄存器存的是你的操作指令(比如READ),两个寄存器存的是参数(比如是地址和size),最后一个寄存器是控制寄存器,在所有的参数都设置好之后向其发出指令,设备开始读取参数,执行命令,程序可能如下:*/
write1(dev.register_size, size);
write1(dev.register_addr, addr);
write1(dev.register_cmd, Read);
write1(dev.register_control, GO);
/*如果最后一条write1被换到了前几条语句之前,那么肯定不是我们所期望的,这时候我们可以在最后一条语句之前加入一个memory barrier,强制cpu执行完前面的写入以后再执行最后一条:*/
write1(dev.register_size, size);
write1(dev.register_addr, addr);
write1(dev.register_cmd, Read);
__sync_synchronize();
write1(dev.register_control, GO); //memory barrier有几种类型:
//acquire barrier : 不允许将barrier之后的内存读取指令移到barrier之前(linux kernel中的wmb())
//release barrier : 不允许将barrier之前的内存读取指令移到barrier之后 (linux kernel中的rmb())
//full barrier : 以上两种barrier的合集(linux kernel中的mb()) //好吧,说实话这个函数的说明基本没看懂
最后从网上找一个代码写一写:http://blog.csdn.net/hzhsan/article/details/25837189
测试场景:假设有一个应用:现在有一个全局变量,用来计数,再创建10个线程并发执行,每个线程中循环对这个全局变量进行++操作(i++),循环加2000000次。
所以很容易知道,这必然会涉及到并发互斥操作。下面通过三种方式[传统互斥量加锁方式, no lock不加锁的方式, 原子函数方式]来实现这种并发操作。并对比出其在效率上的不同之处。
这里先贴上代码,共5个文件:2个用于做时间统计的文件:timer.h timer.cpp。这两个文件是临时封装的,只用来计时,可以不必细看。
//timer.h 用于计时 #ifndef TIMER_H_ |
//timer.cpp |
//thread_function.h -->多线程要调用的函数 |
//thread_function.cpp |
//lock.h --->给mainnolock.cpp使用的类
#ifndef LOCK_H_
#define LOCK_H_
struct LOCK
{
int mutex;
int use;
int unUse;
LOCK() : mutex(), use(), unUse()
{
}
};
#endif
//mainlock.cpp 使用mutex加锁方式的多线程 |
//main_nolock.cpp 使用__sync_compare_and_swap的多线程 |
//main_atomic.cpp 使用__sync_fetch_and_add的多线程 |
//makefile CC = g++
CFLAGS = -g -lpthread -std=c++ OBJS_LOCK = main_lock.o timer.o thread_function.o
OBJS_UNLOCK = main_nolock.o timer.o thread_function.o
OBJS_ATOMICLOCK = main_atomic.o timer.o thread_function.o INC = timer.h thread_function.h lock.h lock : $(OBJS_LOCK) $(INC)
$(CC) -o mainlock $(OBJS_LOCK) $(CFLAGS)
rm *.o nolock : $(OBJS_UNLOCK) $(INC)
$(CC) -o mainnolock $(OBJS_UNLOCK) $(CFLAGS)
rm *.o atomiclock : $(OBJS_ATOMICLOCK) $(INC)
$(CC) -o mainatomic $(OBJS_ATOMICLOCK) $(CFLAGS) main_lock.o : main_lock.cpp
$(CC) -c main_lock.cpp $(CFLAGS) main_nolock.o : main_nolock.cpp
$(CC) -c main_nolock.cpp $(CFLAGS) main_atomic.o : main_atomic.cpp
$(CC) -c main_atomic.cpp $(CFLAGS) timer.o : timer.cpp
$(CC) -c timer.cpp $(CFLAGS) thread_function.o : thread_function.cpp
$(CC) -c thread_function.cpp $(CFLAGS) clean:
rm *.o
执行makefile
make lock
make nolock
make atomiclock
然后生成3个可执行文件
运行这3个可执行文件:
另外:针对main_nolock.cpp而言,作者提到了一个现象
在thread_function.cpp中, 随着一下代码的改变,运行时间会有变化 while (!(__sync_bool_compare_and_swap (&mutex,lock, 1) )); while (!(__sync_bool_compare_and_swap (&mutex,lock, 1) )) usleep(1); while (!(__sync_bool_compare_and_swap (&mutex,lock, 1) ))usleep(10); while (!(__sync_bool_compare_and_swap (&mutex,lock, 1) ))usleep(100); while (!(__sync_bool_compare_and_swap (&mutex,lock, 1) ))usleep(1000); while (!(__sync_bool_compare_and_swap (&mutex,lock, 1) ))usleep(10000); while (!(__sync_bool_compare_and_swap (&mutex,lock, 1) ))usleep(100000); 执行时间的关系是 : T(;)<T(1)<T(10)<T(100)<T(1000)<T(10000)>T(100000) |
通过编程测试及测试得出结论:
1、如果是想用全局变量来做统计操作。而又不得不考虑多线程间的互斥访问的话,最好使用编译器支持的原子操作函数。再满足互斥访问的前提下,编程最简单,效率最高。
2、lock-free,无锁编程方式确实能够比传统加锁方式效率高。所以在高并发程序中采用无锁编程的方式可以进一步提高程序效率。但是得对无锁方式有足够熟悉的了解,不然效率反而会更低而且容易出错。(比如在某些情况下main_nolock比main_lock的效率还要低)
在学习一个无锁化编程的分析帖子 http://blog.csdn.net/hzhsan/article/details/25141421
Lock-free 算法通常比基于锁的算法要好:
- 从其定义来看,它们是 wait-free 的,可以确保线程永远不会阻塞。
- 状态转变是原子性的,以至于在任何点失败都不会恶化数据结构。
- 因为线程永远不会阻塞,所以当同步的细粒度是单一原子写或比较交换时,它们通常可以带来更高的吞吐量。
- 在某些情况下,lock-free 算法会有更少的同步写操作(比如 Interlocked 操作),因此纯粹从性能来看,它可能更便宜。
但是 lock-freedom 并不是万能药。下面是一些很明显的不利因素:
- 乐观的并发使用会对 hot data structures 导致 livelock。
- 代码需要大量困难的测试。通常其正确性取决于对目标机器内存模型的正确解释。
- 基于众多原因,lock-free 代码很难编写和维护。
比较项目
|
无锁编程
|
分布式编程
|
|
1
|
加速比性能
|
取决于竞争方式,除非也采用分布式竞争,否则不如分布式锁竞争的性能
|
加速比和CPU核数成正比关系,接近于单核多任务时的性能
|
2
|
实现的功能
|
有限
|
不受限制
|
3
|
程序员掌握难易程度
|
难度太高,过于复杂,普通程序员无法掌握,目前世界上只有少数几个人掌握。
|
和单核时代的数据结构算法难度差不多,普通程序员可以掌握
|
4
|
现有软件的移植
|
使用无锁算法后,以往的算法需要废弃掉,无法复用
|
可以继承已有的算法,在已有程序基础上重构即可。
|
可在分布计算机系统的几台计算机上同时协调执行的程序设计方法,分布式程序设计的主要特征是分布和通信。采用分布式程序设计方法设计程序时,一个程序由若干个可独立执行的程序模块组成。这些程序模块分布于一个分布式计算机系统的几台计算机上同时执行。分布在各台计算机上的程序模块是相互关联的,它们在执行中需要交换数据,即通信。只有通过通信,各程序模块才能协调地完成一个共同的计算任务。采用分布式程序设计方法解决计算问题时,必须提供用以进行分布式程序设计的语言和设计相应的分布式算法。分布式程序设计语言与常用的各种程序设计语言的主要区别,在于它具有程序分布和通信的功能。因此,分布式程序设计语言,往往可以由一种程序设计语言增加分布和通信的功能而构成。分布式算法和适用于多处理器系统的并行算法,都具有并行执行的特点,但它们是有区别的。设计分布式算法时,必须保证实现算法的各程序模块间不会有公共变量,它们只能通过通信来交换数据。此外,设计分布式算法时,往往需要考虑坚定性,即当系统中几台计算机失效时,算法仍是有效的。
__sync_fetch_and_add的更多相关文章
- [充电]多线程无锁编程--原子计数操作:__sync_fetch_and_add等12个操作
转自:http://blog.csdn.net/minCrazy/article/details/40791795 多线程间计数操作.共享状态或者统计相关时间次数,这些都需要在多线程之间共享变量和修改 ...
- [转] 多线程下变量-gcc原子操作 __sync_fetch_and_add等
http://blog.sina.com.cn/s/blog_6f5b220601013zw3.html 非常好的原子操作,不用加锁:__sync_fetch_and_add GCC 提供的原子操作 ...
- linux无锁化编程--__sync_fetch_and_add系列原子操作函数
linux支持的哪些操作是具有原子特性的?知道这些东西是理解和设计无锁化编程算法的基础. 下面的东西整理自网络.先感谢大家的分享! __sync_fetch_and_add系列的命令,发现这个系列命令 ...
- __sync_fetch_and_add系列
__sync_fetch_and_add系列一共有十二个函数,有加/减/与/或/异或/等函数的原子性操作函数,__sync_fetch_and_add,顾名思义,先fetch,然后自加,返回的是自加以 ...
- __sync_fetch_and_add函数(Redis源码学习)
__sync_fetch_and_add函数(Redis源码学习) 在学习redis-3.0源码中的sds文件时,看到里面有如下的C代码,之前从未接触过,所以为了全面学习redis源码,追根溯源,学习 ...
- C++服务器开发之笔记三
为什么需要原子性操作? 我们考虑一个例子:(1)x++这个常见的运算符在内存中是怎样操作的?从内存中读x的值到寄存器中,对寄存器加1,再把新值写回x所处的内存地址 若是有两个线程同时对同一个变量++, ...
- centos 7.0 编译安装php 7.0.3
php下载页面 http://cn2.php.net/downloads.php 7.0.3多地区下载页面 http://cn2.php.net/get/php-7.0.3.tar.gz/from/a ...
- AliSQL的编译使用
1.下载源码 git clone https://github.com/alibaba/AliSQL.git Linux下编译 2.编译 编译前需要安装好gcc cmake bison等.(如果缺少其 ...
- POCO库——Foundation组件之核心Core
核心Core: Version.h:版本控制信息,宏POCO_VERSION,值格式采用0xAABBCCDD,分别代表主版本.次版本.补丁版本.预发布版本: Poco.h:简单地包含了头文件Found ...
随机推荐
- java_利用session校验图片认证码
RegisterServlet:检验server,client验证码是否一致 ImageServlet: 产生验证码 <!DOCTYPE html> <html> <he ...
- eclipse中不能找到dubbo.xsd解决方法
使用dubbo时遇到问题: org.xml.sax.SAXParseException: schema_reference.4: Failed to read schema document 'htt ...
- php面试常用算法
这些都是真实的IT公司招聘PHP程序员的面试题,这些都是简单的基本算法.包括:冒泡算法.快速排序算法.二分查找算法.顺序算法. 冒泡排序,对象可以是一个数组 01 function bubble_so ...
- ListView中不同类型view的实现
首先创建请求队列,一个活动中只需要一个,因此放在Application中: public class MyApplication extends Application{ private static ...
- Oracle批量加注释,并生成html
excel连接列名生成oracle注释 notes: A2为列名,B2为注释 ="comment on column ColAgreementHeader."&A2& ...
- 深入理解计算机系统第二版习题解答CSAPP 2.18
将32位补码表示的数转换为10进制数. 32位补码 十进制 0x1b8 0x14 0xFFFFFE58 -424 0xFFFFFE74 -396 0x44 0xFFFFFEC8 -312 0x10 0 ...
- 关于Git和SVN的对比
1.git的提交是一个DAG有向无欢图.可以看到哥哥分支之间的合并关系.SVN的提交是一条直线. 2.git的提交版本号不是一个简单递增的数字,而是一个长达40位的十六进制数字(哈希值) 但是可以适用 ...
- JavaScript高级程序设计(第三版)学习笔记13、14章
第13章,事件 事件冒泡 IE的事件叫做事件冒泡:由具体到不具体 <!DOCTYPE html> <html> <head> <title>E ...
- alert
先别着急测试,来猜测一下下面一行代码执行的结果 alert(alert(1234567)); 此刻,我自己还不是不太理解 我的分析是这样: alert() 是window下面的一个方法 alert(1 ...
- mysql查找重复
중복된 것 모두 찾기 SELECT 필드명, count(*) FROM 테이블명 GROUP BY 필드명 mysql> SELECT t1, count(*) FROM tes ...