优化多线程并发性能问题——降低锁的粒度。
案例背景
微视iOS接入QAPM作为项目的性能监控工具已有一年多,打开的功能包括掉帧率、卡顿、SIGKILL、内存触顶、VC泄露和大块内存监控。在QAPM同学的日常运营过程中,发现微视的Tapd在近期某段时间内突然出现有较多的sigkill类型的bug上报,于是借这次案例对sigkill进行一次分析总结。
sigkill监控原理
QAPM的Sigkill监控对用户所产生的crash做了两种类型的分类,一类是FOOM(内存使用过大)引起的crash,一类是Deadlock(死锁)引起的crash。QAPM会对用户内存申请进行预采堆栈,当在APP在前台发生了闪退,就会先判断闪退前是否卡顿超过5S,如果超过5S,则归类为deadlock,否则归类为foom(排除用户没有强退APP,系统没有升级/重启等其它因素)。
分析过程
在这次案例中,微视上报的是deadlock类型的缺陷,bug单如下所示:
由bug单可知,在用户出现死锁的时候,QAPM采集了当时各线程的堆栈信息,第一个含main的为主线程的堆栈调用情况,可以看到在主线程里有多处ws_objc_msgSend的调用,在其它子线程也存在同样的情况,如下图所示:
通过排查代码,发现ws_objc_msgSend这个方法会触发对一个全局bool变量的访问,而多线程访问同一变量容易出现死锁,但从代码上来说那个变量基本不存在并发问题,但死锁卡死的问题还是出现了……。
解决方案
在多线程并发应用中,开发者通常采用的措施就是加锁,虽然这种方式能很好的保证了线程同步问题,但同时也带来了一些性能上的消耗,如上下文切换耗时,内存同步耗时,线程阻塞引起的卡死等。
在由上图的数据分析显示,在影响卡顿的众多因素中,因锁粒度太大引起的卡顿占比为3%,仅次于主线程处理网络回包所带来的卡顿,所以在优化线程并发的性能问题的时候有一个方案可以参考,就是降低锁的粒度。
在微视这次案例中,虽然暂时没找到明确导致线程死锁的原因,但尝试了换一种方式把问题解决:直接去掉semaphore信号量锁,加上了一个volatile修饰。volatile被称为是“最轻量级的锁”,在某些情况下比锁更加方便。一个字段如果被声明为volatile,其中一个线程对它的值进行了修改则会被强制立即更新到主存,当有其他线程需要读取时,它会去主存重新读取新值从而确保所有线程看到这个变量的值是一致的。volatile本质是表示当前变量在寄存器中的值是不确定的,需要从主存中读取,而加锁则是锁定当前变量,只有当前线程可以访问该变量,其它线程被阻塞。另外volatile机制可以避免锁机制引起的线程上下文切换和调度问题,所以volatile的执行成本相对更低。虽然使用volatile变量不会像锁那样造成线程阻塞,但volatile也有其不足之处,就是不具备线程安全,只有在某些特定的条件下才能使用volatile,通常来说,使用volatile必须具备以下2个条件: 1)对变量的写操作不依赖于当前值 2)该变量没有包含在具有其他变量的不变式中 如果大家有更好的建议,期待提供更多应对多线程并发问题的解决方案~
感谢微视iOS项目同学的支持与反馈~ QAPM在不断成长中,欢迎大家多提意见,分享想法!
如有兴趣或任何疑问,请联系在线客服:QAPM