Dreaming-in-Gottingen

原子操作耗时对比

  早前写的原子测试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%。后面需要辅助用其他手段来求证。。。
发表于 2021-03-28 22:52  OnlyTime_唯有时光  阅读(0)  评论(0编辑  收藏
 

相关文章: