热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

浅谈一种规避iOS多线程Crash的方案

一般来说,多线程编程因具有以下几个优点,一直被广泛应用:资源利用率更好程序设计在某些情况下更简单程序响应更快但是因为多线程而导致的cr

一般来说,多线程编程因具有以下几个优点,一直被广泛应用:


  • 资源利用率更好
  • 程序设计在某些情况下更简单
  • 程序响应更快

但是因为多线程而导致的crash问题,也是令程序员非常头疼的一个问题,因为线程调度执行顺序的不确定性,造成了crash一般都是小概率出现,在开发测试阶段很难发现,而一旦上线面对用户,造成的影响却是不容小觑的。


一、Crash的场景

有一种特别常见的会造成crash的场景为:多线程读写可变数组/字典

我们来看几个简单的测试代码,看看到底在什么情况下会引起Crash:

// 1. 多线程只读数据
NSMutableDictionary * dict1 = [[NSMutableDictionary alloc] init];
[dict1 setObject:@"test0" forKey:@"test0"];
[dict1 setObject:@"test1" forKey:@"test1"];
[dict1 setObject:@"test2" forKey:@"test2"];
for (int i = 0; i <1000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{NSString * key = [NSString stringWithFormat:@"test%d", i%3];NSString * value = dict1[key];NSLog(@"%@:%@", key, value);});
}// 2. 多线程只写数据
// 字典仅有一个key
// 多线程写同一个key
NSMutableDictionary * dict2 = [[NSMutableDictionary alloc] init];
for (int i = 0; i <1000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{[dict2 setObject:@(i) forKey:@"test"];});
}// 3. 多线程只写数据
// 字典有多个key
// 多线程同时写不同key
NSMutableDictionary * dict3 = [[NSMutableDictionary alloc] init];
for (int i = 0; i <1000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{[dict3 setObject:[NSString stringWithFormat:@"test%d", i] forKey:[NSString stringWithFormat:@"test%d", i]];});
}// 4. 多线程只写数据
// 字典有多个key
// 多线程随机写key
NSMutableDictionary * dict4 = [[NSMutableDictionary alloc] init];
for (int i = 0; i <1000; i++) {int x = arc4random() % 3;dispatch_async(dispatch_get_global_queue(0, 0), ^{[dict4 setObject:[NSString stringWithFormat:@"test%d", i] forKey:[NSString stringWithFormat:@"test%d", x]];});
}// 5. 多线程读写数据
NSMutableDictionary * dict5 = [[NSMutableDictionary alloc] init];
for (int i = 0; i <1000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{[dict5 setObject:[NSString stringWithFormat:@"test%d", i%3] forKey:[NSString stringWithFormat:@"test%d", i%3]];});dispatch_async(dispatch_get_global_queue(0, 0), ^{NSString * key = [NSString stringWithFormat:@"test%d", i%3];NSString * value = dict5[key];NSLog(@"%@:%@", key, value);});
}

 注:可适当将测试次数加大,以增加复现概率。

测试结果发现:


  1. 多线程只读数据情况下:不会发生Crash;
  2. 多线程只写数据,字典仅有一个key,且多线程读写同一个key的情况下,会发生Crash;
  3. 多线程只写数据,字典有多个key,且多线程同时写不同key的情况下,会发生Crash;
  4. 多线程只写数据,字典有多个key,多线程随机写key的情况下,会发生Crash;
  5. 多线程读写数据的情况下,会发生Crash。

其中:

2 - 4 情况下的崩溃堆栈,均含有关键字:-[__NSDictionaryM setObject:forKey:]

5情况下,则为:-[__NSDictionaryM objectForKeyedSubscript:]

除此之外,还有其他可能的堆栈表现形式:

0 libsystem_kernel.dylib __pthread_fchdir
1 libsystem_c.dylib abort 
2 libsystem_malloc.dylib free
3 CoreFoundation _mdict_rehashd
4 CoreFoundation -[__NSDictionaryM setObject:forKey:]

如果在多线程读写时进行了遍历操作,则Crash堆栈则可能为:

0 CoreFoundation ___exceptionPreprocess
1 libobjc.A.dylib objc_exception_throw
2 CoreFoundation _CFArgv
3 CoreFoundation -[__NSPlaceholderArray initWithObjects:count:]
4 CoreFoundation +[NSArray arrayWithObjects:count:]
5 CoreFoundation -[NSDictionary allKeys]

可以看到只有多线程只读的情况下,不会引起Crash,其他情况下都会,包括有:


  • 多线程只写
  • 多线程读写

二、Crash解决方案

那如何规避多线程下操作可变数组/字典的Crash问题呢?一般来说,保证多线程安全的常用方法主要有以下几种:


  1. @property使用atomic属性
  2. @synchronized(token)
  3. NSLock
  4. GCD

1. @property使用atomic属性

在这个情况下,atomic属性会为变量生成原子操作的getter和setter方法,听上去这样就可以解决了多线程的读写Crash问题,事实真是如此吗?我们来看下面这个例子:

@property (atomic, strong) NSArray* arr;dispatch_async(dispatch_get_global_queue(0, 0), ^{//thread Afor (int i = 0; i <100000; i ++) {if (i % 2 == 0) {self.arr = @[@"1", @"2", @"3"];} else {self.arr = @[@"1"];}NSLog(@"Thread A: %@\n", self.arr);}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{//thread Bfor (int i = 0; i <100000; i ++) {if (self.arr.count >= 2) {NSString * str = [self.arr objectAtIndex:1];}NSLog(@"Thread B: %@\n", self.arr);}
});

 即使B线程在访问objectAtIndex之前做了count的判断,依旧发生了Crash:

#0 __pthread_kill
#1 pthread_kill
#2 abort
#3 abort_message
#4 default_terminate_handler()
#5 _objc_terminate()
#6 std::__terminate(void (*)())
#7 __cxa_throw
#8 objc_exception_throw
#9 -[__NSArrayI objectAtIndex:]

原因也是由于前后两行代码之间arr所指向的内存区域被其他线程修改了。

所以在大部分多线程情况下,即使声明为atomic也没有用,atomic的作用只是给getter和setter加了个锁,保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了,在需要做多线程安全的场景,自己去额外加锁做同步。


2. @synchronized(token) & NSLock

还是如上的例子,我们加上@synchronized看看Crash情况如何:

dispatch_async(dispatch_get_global_queue(0, 0), ^{@synchronized(self) {//thread Afor (int i = 0; i <100000; i ++) {if (i % 2 == 0) {self.arr = @[@"1", @"2", @"3"];} else {self.arr = @[@"1"];}NSLog(@"Thread A: %@\n", self.arr);}}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{@synchronized(self) {//thread Bfor (int i = 0; i <100000; i ++) {if (self.arr.count >= 2) {NSString * str = [self.arr objectAtIndex:1];}NSLog(@"Thread B: %@\n", self.arr);}}
});

 经过测试发现,Crash没有了!

同样使用NSLock效果一样,可以看到这两种方法是行之有效的,但是这就足够了吗?


3. GCD

GCD(Grand Central Dispatch),这个是苹果为多核的并行运算提出的解决方案,会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),开发者只需要告诉 GCD 想要如何执行什么任务,不需要编写任何线程管理代码。

同样如上的例子,我们使用GCD后,看看使用效果:

dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(dispatch_get_global_queue(0, 0), ^{//thread Afor (int i = 0; i <100000; i ++) {dispatch_async(serialQueue, ^{if (i % 2 == 0) {self.arr = @[@"1", @"2", @"3"];} else {self.arr = @[@"1"];}NSLog(@"Thread A: %@\n", self.arr);});}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{//thread Bfor (int i = 0; i <100000; i ++) {dispatch_async(serialQueue, ^{if (self.arr.count >= 2) {NSString * str = [self.arr objectAtIndex:1];}NSLog(@"Thread B: %@\n", self.arr);});}
});

 同样的,Crash没有了!


4. 对比

由上,我们有3种有效的保证多线程读写安全的方法,那么应该如何选择呢?


  • @synchronized(token) 使用简单,可读性强,但是性能消耗大
  • NSLock性能消耗小于@synchronized(token) ,但使用不当,可能引起死锁
  • GCD性能最优,且在复杂代码逻辑下的多线程读写时,可有效保障所需代码的串行执行,避免逻辑出错。

以本人所开发的业务某段逻辑的效率真实对比:

 

可以看到使用dispatch_sync/dispatch_async相比于@synchronized,性能提升10+倍


5. 最终方案

核心思想为:保证同一数组/字典的操作按顺序执行

写方法使用dispatch_async,读方法使用dispatch_sync,例如:

_ioQueue = dispatch_queue_create("ioQueue", DISPATCH_QUEUE_SERIAL);- (void)setSafeObject:(id)object forKey:(NSString *)key {key = [key copy];dispatch_async(self.ioQueue, ^{if (key && object) {[_dic setObject:object forKey:key];}});
}- (id)getSafeObjectForKey:(NSString *)key {__block id result = nil;dispatch_sync(self.ioQueue, ^{result = [_dic objectForKey:key];});return result;
}

 如果想进一步提升读写效率,可考虑只一个线程写,但允许多线程读:

_ioQueue = dispatch_queue_create("ioQueue", DISPATCH_QUEUE_CONCURRENT);- (void)setSafeObject:(id)object forKey:(NSString *)key {key = [key copy];// 会等待barrier之前的block执行完成后才执行dispatch_barrier_async(self.ioQueue, ^{if (key && object) {[_dic setObject:object forKey:key];}});
}- (id)getSafeObjectForKey:(NSString *)key {__block id result = nil;dispatch_sync(self.ioQueue, ^{result = [_dic objectForKey:key];});return result;
}

 笔者目前使用更多的是第一种写法,使用串行队列,因为使用起来更不易出错,且目前性能消耗已经非常低,已满足要求,后续待业务版本功能稳定后,会考虑切换到第二个写法,真实感受是否第二种写法效率更加好。


推荐阅读
  • 来自微信官方:微信支付跨平台软件架构首次曝光
    大纲背景线上效果指标什么是软件架构为什么需要软件架构从零到一构建支付跨平台软件架构1.抽象业务流程2.加入路由机制3.管理网络请求4.规范数据传递总结背景作为一个重要业务ÿ ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 本文介绍了Java集合库的使用方法,包括如何方便地重复使用集合以及下溯造型的应用。通过使用集合库,可以方便地取用各种集合,并将其插入到自己的程序中。为了使集合能够重复使用,Java提供了一种通用类型,即Object类型。通过添加指向集合的对象句柄,可以实现对集合的重复使用。然而,由于集合只能容纳Object类型,当向集合中添加对象句柄时,会丢失其身份或标识信息。为了恢复其本来面貌,可以使用下溯造型。本文还介绍了Java 1.2集合库的特点和优势。 ... [详细]
  • monkey初接触
    第一次听说monkey,根本不知道是什么东西,脑海里就一个印象,很厉害的自动化测试工具,可是体验了一下,似乎不 ... [详细]
  • Iamworkingonaprojectwhichrequiresopentokandcallkitfornotifyingusers.However,theappli ... [详细]
  • iOS开发Debug和Release的理解
    2019独角兽企业重金招聘Python工程师标准参考:http:blog.csdn.netmad1989articledetails406580331&# ... [详细]
  • 这篇论文跟普通的论文是区别的,它并不是针对现有问题,提出一个新颖的解决方案,然后对其进行测试评估。这篇论文主要是对文件系统的代码发展做了一 ... [详细]
  • ios中级面试题(二)
    1.如何追踪app崩溃率,如何解决线上闪退当iOS设备上的App应用闪退时,操作系统会生成一个crash日志,保存在设备上。crash日志上有很多有用的信息,比如每个正在执行线程的 ... [详细]
  • 【转】Android 性能优化之内存检测、卡顿优化、耗电优化、APK瘦身
    原文(https:blog.csdn.netcsdn_aiyangarticledetails74989318)导语自2008年智能时代开始,Android操作系统一路高歌,10年 ... [详细]
author-avatar
赵娜supergirl
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有