货拉拉iOS司机端线程治理总结

司机组iOS 团队,担任国内货运司机端 iOS APP 开发,一起支撑国内 iOS 事务线的事务基础架构的开发和保护。

背景介绍

  • 因为在曩昔几年,货拉拉事务高速发展的一起,作为核心事务入口的司机端,同样在以「快」为榜首目标完结事务需求迭代,积累了较多的技术债(各项技术指标与业界优秀的app比较都差强人意),并且线上经常会收到司机反馈手机发烫,耗电,crash等等问题。
  • 司机运用的手机比较用户来说功能遍及较差,一起司机的在线时长较高(均匀3.5小时),因为以上客观原因的存在,给司机端功能优化带来了巨大的应战。

综上,线程管理专项应运而生,意图便是下降crash,手机发烫,耗电等问题,尽量给本来并不富裕的内存,雪中送炭。

问题分析

货拉拉iOS司机端线程治理总结

  1. 乱用运用大局行列,并且运用了行列的默许优先级

dispatch_async(dispatch_get_global_queue(0, 0), ^{
       //TODO
});

开发人员在需求敞开线程处理使命时,大多都采用了大局行列默许优先级来处理,所以项目中积累了很多的大局行列默许优先级,导致了一人干活,全家围观。

  1. 很多不必要的线程切换

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //loadData
    dispatch_async(dispatch_get_main_queue(), ^{
      //改写UI
    });
});

一般的事务处理中,用子线程的确能够进步使命处理效率,可是也不能忽视行列切换带来的功能损耗。假如loaddata,是较为耗时的操作,用子线程处理无可厚非,可是仅仅是为了读取本地的一些简单装备或许数据,而敞开线程,就有点剩余了。(这里的loaddata,需求依据本身事务进行评价,是否有必要敞开)

  1. 在高并发场景,没有控制并发,而运用了大局行列创立了很多线程

//实时获取方位信息 异步
- (void)getDriverCurrentAddress:(void(^)(HLLAddressComponent *component))complete
{
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
    __block CLLocation *location;
    __block NSDictionary *regeoInfo;
       //事务处理
  });
}

多个事务恳求需求依靠getDriverCurrentAddress异步回来的数据,所以会导致,多个getDriverCurrentAddress并发,然而办法内部并未控制并发,并且还采用了大局行列默许优先级,当业并发大的时分,这里会偶现死锁。

  1. 事务运用线程的不合理

 dispatch_async(dispatch_get_global_queue(0, 0), ^{
     NSMutableArray<NSDictionary *> *imageArray = [NSMutableArray array];
    for (NSDictionary *photoDict in readyUploadImageArray) {
      // 上传相片
    }
 });

事务运用线程不合理,事务要求是一切需求上传的图片,并发上传。实际上大局行列默许优先级分配一个线程后,多个使命挤在一个线程,并未到达事务预期的意图。

  1. 线程死锁引起的crash

当大面积呈现psynch_cvwait,semwait_signal,psynch_mutexwait,psynch_mutex_trylock,dispatch_sync_f_slow等信息时,能够初步判定为线程死锁。比方:

货拉拉iOS司机端线程治理总结

当然优先级回转也会导致死锁,具体来说,假如一个低优先级的线程取得锁并访问共享资源,这时一个高优先级的线程也尝试取得这个锁,它会处于 spin lock 的忙等状况然后占用很多 CPU。此刻低优先级线程无法与高优先级线程争夺 CPU 时刻,然后导致使命迟迟完不成、无法开释 lock。导致陷入死锁 。

  1. 子线程改写UI引起的crash

子线程改写UI的问题,有比较具体的提示信息,仍是比较简单发现的。

货拉拉iOS司机端线程治理总结

货拉拉iOS司机端线程治理总结

  1. 线程安全引发的crash

因为多线程读写问题的crash比较隐秘,发现难,定位难,所以,当呈现pthread_kill,_objc_release,malloc: error for object 0x7913d6d0: pointer being freed was not allocated等信息时,能够初步判定为多线程读写问题。

仅仅光靠这些仍是不够的,假如没有做特别的行列处理,仍是要做很多的调试,假如发现某处事务或许会被多个线程访问时,也需求要点关注。

方案介绍

  1. 采纳新的行列管理和分配制度

大局行列默许优先级dispatch_get_global_queue(0, 0)的乱用,导致了一人干活,全组围观。当很多并发的事务运用了大局行列的默许优先级时,会为此优先级创立远超CPU核数的线程,不仅让CPU疲于奔命,一起还增加了形成线程死锁危险,然后引发crash。

因为货拉拉的事务特色,咱们决定为不同的优先级,创立与CPU核数持平的串行行列,经过优先级的合理运用和串行行列的调度,充分利用时刻片和多核的效率,一起不呈现相关副作用的状况下完结多线程操作。

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
 @interface HLLQueuePool : NSObject
//与用户交互的使命,这些使命一般跟UI等级的改写相关,比方动画,cell高度,frame等UI的核算
extern dispatch_queue_t HLLQueueForQoSUserInteractive(void);
//由用户发起的并且需求立即得到成果的使命,比方读取数据(装备,用户信息等)来加载UI,会在几秒或许更短的时刻内完结
extern dispatch_queue_t HLLQueueForQoSUserInitiated(void);
//一些耗时的使命,比方杂乱的组合的网络恳求,图片下载,上传
extern dispatch_queue_t HLLQueueForQoSUtility(void);
//对用户不行见,能够长时刻在后台运转,比方,拉取装备,地理方位上报,日志上报等
extern dispatch_queue_t HLLQueueForQoSBackground(void);
//默许,不引荐作为首选运用
extern dispatch_queue_t HLLQueueForQoSDefault(void);
 @end
NS_ASSUME_NONNULL_END

事务运用改动小,只需在原有基础上依据事务特色,弥补合理的优先级即可。

dispatch_async(HLLQueueForQoSUserInitiated(), ^{
    //垃圾机型,读取data,或许会导致卡顿,所以加了个线程。
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urlString]];
});
  1. 整理线程并发较大的事务进行重构

事务场景: 当司机端的事务恳求依靠getDriverCurrentAddress异步回调的数据

当很多的事务并发,调用getDriverCurrentAddress时,getDriverCurrentAddress办法内部采用大局行列(默许的优先级)生成很多线程去处理数据,然后形成死锁或许线程资源耗尽,crash。

事务重构:

  1. 整理事务,恰当下降并发乃至规避并发。
  2. 当事务并发调用getDriverCurrentAddress时,假如有该事务数据缓存,则直接回来,一起获取新的数据并缓存。假如没有事务缓存,则getDriverCurrentAddress内部只能有一个使命履行,其他的使命需等候回调后一并回来。
  1. 线程运用的合理性评价与改造

多线程能够进步体系资源利用率,可是敞开多线程需求花费时刻(90奇妙)和空间(0.5兆),敞开的线程过多,CPU频繁的在多个线程中调度会耗费很多的CPU资源,会导致个别线程无法完结使命而假死,并且简单形成数据同步和死锁的问题,所以不要在体系中一起敞开过多的子线程。

线程运用的合理性评价标准:

  1. 不行预估完结时刻的使命,比方图片上传下载,普通接口恳求
  2. 核算量比较大的,比方加解密,数据核算和处理
  3. 有或许卡顿主线程的使命,比方UI的核算与烘托
  4. 如无必要,不要随意敞开线程。
  1. 死锁问题的要点攻坚

首要咱们要了解线程的生命周期:

  1. 新建:实例化线程目标
  2. 安排妥当:向线程目标发送start消息,线程目标被参加可调度线程池等候CPU调度。
  3. 运转:CPU 担任调度可调度线程池中线程的履行。线程履行完结之前,状况或许会在安排妥当和运转之间来回切换。安排妥当和运转之间的状况变化由CPU担任,程序员不能干涉。
  4. 堵塞:当满意某个预订条件时,能够运用休眠或锁,堵塞线程履行。sleepForTimeInterval(休眠指定时长),sleepUntilDate(休眠到指定日期),@synchronized(self):(互斥锁)。
  5. 逝世:正常逝世,线程履行完毕。非正常逝世,当满意某个条件后,在线程内部中止履行/在主线程中止线程目标

然后,因为死锁问题比较荫蔽,一般很难发现然后去排查,咱们只能经过在bugly和内部的crash体系上,分析仓库信息:

货拉拉iOS司机端线程治理总结

当发现线程大面积的仓库呈现了psynch_cvwait,semwait_signal,psynch_mutexwait,psynch_mutex_trylock,dispatch_sync_f_slow等信息时,就能够大胆置疑线程非正常原因堵塞,而导致的死锁。

最终,因为线程是一把双刃剑,不运用线程就不会形成死锁,就需求依据仓库信息排查对应的事务:

  1. 锁用的是否合理
  2. 线程的数量是否远超平时的线程数
  3. 是否运用了NSRecursiveLock,此递归锁不支持多线程递归,因为会形成优先级回转
  4. 排查事务,线程长时刻的堵塞,导致使命无法正常履行,也会形成死锁
  5. SCNetworkReachabilityGetFlags,此办法只能在子线程调用,否则会形成主线程同步堵塞
  1. 子线程改写UI的要点排查与管理

为什么子线程刷UI,仅仅偶现crash呢?因为在苹果现有框架下,改写UI是一种线程不安全的操作,所以必须放在主线程。放在子线程,刚好竞赛同一资源时,才会crash。

所以需求对以下场景,做统一检查处理

  1. h5交互的回调
  2. 二方库,三方库的代理和回调
  3. 告诉
  4. kvo相关
  5. 接口回调

因为告诉和kvo的触发和处理都在同一线程,假如子线程触发,那么就有或许子线程改写UI

  1. 线程安全问题的整理与重构

线程安全问题的本质,便是多线程写的问题,严谨的说,多线程读并不会形成线程安全问题,因为仅仅读取数据,并不会产生错误的成果,即使交织履行读取,最终成果也是正确的。

cpu读写内存是经过数据总线操作的,且只要一个。所以在涉及到多线程读写问题时,对一切的写进行串行或许加锁操作即可,不需求差异数据类型(虽然基本数据类型,多线程写,不会有问题)。

简单提一下锁和串行行列的差异,锁中间的履行操作相当于是串行行列。锁的特色是,确定范围越小越好,可是锁会形成死锁。gcd串行行列,则不会有死锁的问题。关于用法,仁者见仁。

最终,线程安全问题,乃至比死锁问题还要固执,坚强。因为祖传代码的原因,不得不对多个事务大类,进行了重构,将数据模型进行了拆分,一起对写这一块做了锁或许串行的操作。

长效机制的树立

线程问题比较头疼,在事务迭代和重构的过程中比较简单呈现,如何才能下降线程问题对事务和功能的影响呢?

  1. 树立线程数量监控预警体系

pthread库中供给了一个用于监控线程创立、运转、结束、毁掉的内省函数。

typedef void (*pthread_introspection_hook_t)(unsigned int event, pthread_t thread, void *addr, size_t size);

在发动时,能够选择发动监控,开端监控线程数量。

enum {
    PTHREAD_INTROSPECTION_THREAD_CREATE = 1, //创立线程
    PTHREAD_INTROSPECTION_THREAD_START, // 线程开端运转
    PTHREAD_INTROSPECTION_THREAD_TERMINATE,  //线程运转终止
    PTHREAD_INTROSPECTION_THREAD_DESTROY, //毁掉线程
};

经过线程状况改动,来记录线程数量。

void pthread_introspection_hook_t(unsigned int event,
pthread_t thread, void *addr, size_t size)
{
    //创立线程,则线程数量和线程增长数都加1
    if (event == PTHREAD_INTROSPECTION_THREAD_CREATE) {}
    //毁掉线程,则线程数量和线程增长数都减1
    else if (event == PTHREAD_INTROSPECTION_THREAD_DESTROY){}
}

预警上报

  • 当线程数量大于设定的某一阈值时(各事务,依据自己的事务状况进行阈值设定,一般采用均匀值),采纳预警。
  • 考虑到获取线程仍是比较耗费功能的,所以榜首阶段,在debug阶段,经过控制台预警,打印,看看运用状况和作用。
  • 后续经过APM收集上报
  1. 子线程改写UI检测

Main Thred Checker (Runtime Issue)

货拉拉iOS司机端线程治理总结

除此之外,需求在xcode中,新增一个断点“Main Thred Checker (Runtime Issue)”

假如有UI溃散,溃散点就会呈现在UI溃散的方位。

除此之外,项目在运转时,也能够利用日志重定向匹配Main Thread Checker:最初的错误日志弹框提示。

  1. 标准线程运用

  • 事务一切的线程同一运用HLLQueuePool来进行调度,一起设定好和事务匹配的优先级即可,不需求关心调度。
  • 代码review。有线程相关的修正或许提交,需求阐明,侧重review。

复盘& 总结

货拉拉iOS司机端线程治理总结

本方案于4月份开端落地上线至今,经过数据采集和分析:

  • 涉及到的crash数量大约在26k左右,粗略核算下降了crash率万分之8
  • 线程的均匀数量从之前的51.3,下降到现在的41.6,线程损耗大约是原来的81%,功能节省了大约18.7%

线程管理专项的意图,便是下降crash和功能损耗,从复盘数据来看,crash修复状况和功能优化均契合预期。

本次主要从行列的管理和分配,高并发事务的整理和重构,线程运用的合理性评价与改造,线程相关crash的排查和修复,长效机制的树立几个方面介绍了货拉拉iOS司机端在线程管理方面的实践

希望咱们团队遇到的问题以及解决的经验,能够在稳定性管理方面协助到你。