热门标签 | 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;
}

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


推荐阅读
  • 深入解析CAS机制:全面替代传统锁的底层原理与应用
    本文深入探讨了CAS(Compare-and-Swap)机制,分析了其作为传统锁的替代方案在并发控制中的优势与原理。CAS通过原子操作确保数据的一致性,避免了传统锁带来的性能瓶颈和死锁问题。文章详细解析了CAS的工作机制,并结合实际应用场景,展示了其在高并发环境下的高效性和可靠性。 ... [详细]
  • JUC(三):深入解析AQS
    本文详细介绍了Java并发工具包中的核心类AQS(AbstractQueuedSynchronizer),包括其基本概念、数据结构、源码分析及核心方法的实现。 ... [详细]
  • IOS Run loop详解
    为什么80%的码农都做不了架构师?转自http:blog.csdn.netztp800201articledetails9240913感谢作者分享Objecti ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • MySQL 5.7 学习指南:SQLyog 中的主键、列属性和数据类型
    本文介绍了 MySQL 5.7 中主键(Primary Key)和自增(Auto-Increment)的概念,以及如何在 SQLyog 中设置这些属性。同时,还探讨了数据类型的分类和选择,以及列属性的设置方法。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 本文总结了一些开发中常见的问题及其解决方案,包括特性过滤器的使用、NuGet程序集版本冲突、线程存储、溢出检查、ThreadPool的最大线程数设置、Redis使用中的问题以及Task.Result和Task.GetAwaiter().GetResult()的区别。 ... [详细]
  • 本文将详细介绍如何在Mac上安装Jupyter Notebook,并提供一些常见的问题解决方法。通过这些步骤,您将能够顺利地在Mac上运行Jupyter Notebook。 ... [详细]
  • 本地存储组件实现对IE低版本浏览器的兼容性支持 ... [详细]
  • 本文是Java并发编程系列的开篇之作,将详细解析Java 1.5及以上版本中提供的并发工具。文章假设读者已经具备同步和易失性关键字的基本知识,重点介绍信号量机制的内部工作原理及其在实际开发中的应用。 ... [详细]
  • 深入解析 Synchronized 锁的升级机制及其在并发编程中的应用
    深入解析 Synchronized 锁的升级机制及其在并发编程中的应用 ... [详细]
  • 在Linux系统中,为了提高安全性,可以通过设置命令执行超时和用户超时注销来防止因用户长时间未操作而带来的安全隐患。具体而言,可以通过编辑 `/etc/profile` 文件,添加或修改相关参数,确保用户在指定时间内无操作后自动注销。此外,还可以利用 `timeout` 命令来限制特定命令的执行时间,进一步增强系统的稳定性和安全性。 ... [详细]
  • 优化后的标题:深入解析09版Jedis客户端
    深入解析09版Jedis客户端,本文将详细介绍如何在Java项目中正确配置Jedis以操作Redis。首先,确保项目的JDK版本和编译器设置正确。接着,通过Maven或Gradle导入必要的依赖项,如 `redis.clients:jedis`。此外,文章还将探讨Jedis连接池的配置与优化,以及常见问题的解决方案,帮助开发者高效使用Jedis进行Redis操作。 ... [详细]
  • 在CentOS 6.5环境中,本文详细介绍了如何配置SSH无密钥登录,并成功执行PSSH命令。首先,确保系统已安装PSSH工具,可使用 `yum install pssh` 进行安装。若未配置免密钥登录,PSSH命令将无法正常执行,例如尝试运行 `pssh -H root@192.168.245.129 -i uptime` 时会失败。通过生成并分发SSH公钥,可以实现无密码登录,从而顺利执行PSSH命令。此外,本文还提供了详细的步骤和常见问题的解决方案,帮助用户顺利完成配置。 ... [详细]
  • 本文深入探讨了IO复用技术的原理与实现,重点分析了其在解决C10K问题中的关键作用。IO复用技术允许单个进程同时管理多个IO对象,如文件、套接字和管道等,通过系统调用如`select`、`poll`和`epoll`,高效地处理大量并发连接。文章详细介绍了这些技术的工作机制,并结合实际案例,展示了它们在高并发场景下的应用效果。 ... [详细]
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社区 版权所有