原子操作耗时对比
早前写的原子测试demo,一直挂在心头准备来篇介绍文章。
今天在编译服务器中找了半天还是没找到,最后总算在个人PC中找到了,再不做总结的话可能哪天真会不小心误删了。
代码已上传到这里,有需要的可以拿去测试(本人的demo大多基于Android4.4进行编译和调试,在其他版本上可能出现编译问题)。
1. 背景
多个线程访问同一个资源时,出现并发访问(读和写)问题,如果对资源的管理不合理,可能出现跟预期不同的结果。
例如,多个线程同时对某个全局变量进行自增操作,这个自增操作至少涉及三条指令:
- 从内存单元读入寄存器
- 在寄存器中对变量操作(加/减1)
- 把新值写回到内存单元
如果一个线程执行上面三个步骤过程中,其他线程进入,则会打断前一个的执行动作,导致结果不为预期值。
因此,需要将这几个动作作为原子操作,来加以保护。
2. Android平台下的几种锁使用(以自增为例)
2.1. cutils中实现的锁android_atomic_inc()
其实是对android_atomic_add()的一层封装,参考这里。
2.2. bionic中实现的锁__atomic_inc()
其实是对__sync_fetch_and_add()的一层封装,参考这里。
2.3. bionic中实现的互斥锁pthread_mutex_lock()
其实现参考这个文件。
这个与前两个使用场景有些差异,前面两个主要用于对某个内存变量进行原子操作,这个除具有这个功能外,还可以保护临界资源,
使先拿到锁的线程先运行,只有当释放锁后其他线程才能访问。
3. 性能对比
这几种锁的性能如何呢?参考demo,接下来分几种场景我对其进行对比。
测试手段:10个线程,同时操作同一个全局变量,操作次数为50k次,测算耗时。
3.1. 使用cutils实现的原子操作函数:
3.2. 使用bionic中实现的原子操作函数(其实是gcc build-in api):
3.3. 使用常用的pthread_mutex_t来保护全局变量:
测试inline:(无函数调用开销,速度快)
测试no-inline:(有函数调用开销,速度稍慢)
3.4. 不使用任何锁来保护:(结果可能出错)
3.5. 不使用任何锁来保护+sleep:(结果一定出错)
3.6. pthread_mutex_lock+sleep:
经以上对比,可以得到,速度方面:__atomic_inc() > android_atomic_inc() > pthread_mutex_lock()
其他补充:
- 编译器gcc的build-in实现__sync_fetch_and_add()性能最好。
- 测试机器硬件平台为Cortex-A53。
- inline函数因为少了函数调用开销,速度较快,但会导致代码膨胀,因为其是在“预-编-汇-链”步骤中的预处理阶段进行了代码展开。
- 对变量自增操作,如果不加锁或不用原子操作,结果仍有可能正确,毕竟自增操作的几条指令被别的线程打断的概率非常低。 但是,不加锁并且sleep,被打断的概率非常大,参考3.5的测试结果,导致结果出错。
- 加sleep()会导致耗时大大增加,因为其休眠时长不精确,再扩大50k倍后,导致耗时非常大。但是,usleep(1)不精确,即使扩大5倍后,单个线程执行耗时为:5us*50k=250ms,最后10个线程累加为:250ms*10=2.5s,但是3.5/3/6总耗时大约为:39s,因此猜测得出下条的结论:
- sleep是线程被调用时,让出系统资源,放弃当前cpu时间片并阻塞指定时间,让其他线程可以占用cpu。因此,耗时增加如此之多,大概就是大部分时间耗在了线程上下文切换上,每自增一次,线程抢占一次。但是,这个分析可能有误,因为top看cpu占用时,远低于100%,如果不sleep的话,基本上就是100%。后面需要辅助用其他手段来求证。。。