Created
June 29, 2016 03:11
-
-
Save xiayun200825/c5949dceb7b283d157ad3ab9c3c4766c to your computer and use it in GitHub Desktop.
iOS并发编程最佳实践之资源共享
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
iOS并发编程最佳实践之资源共享 | |
=================== | |
从一个crash说起 | |
------------- | |
首先来看一个crash日志。如下: | |
``` | |
Thread 1 Crashed: | |
...... | |
4 CoreFoundation 0x00000001814e4d6c ___65-[__NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:]_block_invoke :120 (in CoreFoundation) | |
5 CoreFoundation 0x00000001814e4c20 -[__NSDictionaryM enumerateKeysAndObjectsWithOptions:usingBlock:] :144 (in CoreFoundation) | |
...... | |
10 HTao 0x0000000100143200 +[TaffySerializeUtil toJSONData:] TaffySerializeUtil.m:14 (in HTao) | |
11 HTao 0x0000000100143260 +[TaffySerializeUtil toJSONString:] TaffySerializeUtil.m:22 (in HTao) | |
12 HTao 0x00000001001f4434 -[TaffyKeeper sendRequest] TaffyKeeper.m:321 (in HTao) | |
``` | |
排除系统方法,与我们直接相关的代码在第12行`[TaffyKeeper sendRequest]`中对`[TaffySerializeUtil toJSONString:]`的方法调用。该处方法调用如下: | |
``` | |
// 在interface中定义的属性 | |
@property(nonatomic, strong) NSMutableDictionary *registerList; | |
// 发生crash的语句 | |
NSString *requestKeyset = [TaffySerializeUtil toJSONString:self.registerList]; | |
``` | |
结合第4行`enumerateKeysAndObjectsWithOptions:usingBlock`可以发现,错误发生在对一个`NSMutableArray`的对象进行遍历操作时。什么情况下,遍历`NSMutableArray`会发生错误呢?当它在遍历时,数组中元素发生了改变,例如新增或删除元素。通常在发生类似异常时,我们需要考虑下是否是多线程对同一对象的操作带来的影响。 | |
iOS中提供了几种不同的API来实现多线程编程。其中,GCD更是因其灵活方便的特性,容易让开发者在快乐地使用中,忘记并发编程中的许多注意事项和陷阱。而在并发编程中,最核心的问题就是操作线程间的共享资源。 | |
资源共享 | |
------------- | |
听上去这是一个老生常谈的问题,那么让我们站在客户端的角度,看看有哪些不一样的发现。 | |
首先,定义在iOS中可能发生共享冲突的资源。我们访问的每一个属性,每一个对象,都需要操心在多线程中可能存在的冲突问题吗?幸运的是,我们不需要。Cocoa库帮我们做了许多工作,它把对象简单地分成**线程安全**和**非线程安全**两类。对于线程安全对象,在线程间不需加锁即可访问同一实例;而对于非线程安全对象,则需要考虑在线程间的共享冲突。 | |
哪些是非线程安全对象?参考Apple文档[线程安全概览][1],如下: | |
> **Guidelines** | |
> | |
> - Immutable objects are generally thread-safe. Once you create them, you can safely pass these objects to and from threads. On the other hand, mutable objects are generally not thread-safe. | |
> - Many objects deemed “thread-unsafe” are only unsafe to use from multiple threads. Many of these objects can be used from any thread as long as it is only one thread at a time. | |
总体来说, | |
1. **不可变对象都是线程安全的**,如`NSArray`,`NSDictionary`等;而可变对象则是非线程安全的,如`NSMutableArray`、`NSMutableDictionary`、`NSMutableString`,在线程间使用时需要被合理的同步。 | |
2. **即使是非线程安全对象,只要保证同一时间只有一个线程访问,也是没问题的**。这是什么样的场景呢?设想一下在GCD中串行队列的工作场景,队列中的任务可能被分发到不同的线程上执行,但是同一时间,只会有一个任务在某一条线程上运行。这种情况下,访问非线程安全对象,而不做任何保护措施是可以的。最常见的例子即GCD中的`MainQueue`,大部分时候,我们的代码都是写在主线程,而很少需要考虑到资源冲突,原因正是在于此。 | |
> **TIps**: | |
> | |
> - 这里有一个很tricky的地方值得注意。即对返回线程安全的对象保持怀疑态度,例如方法声明返回`NSArray`,但有可能实际返回`NSMutableArray`对象。比较好的做法是在方法返回或接收对象时加上`[array copy]`来确保取得的是不可变对象。 | |
> - 其他串行队列不一样的是,`MainQueue`只对应一条线程即主线程。 | |
资源保护 | |
------------- | |
那么对于非线程安全对象,应该如何保护呢?你可能会立即想到使用**`@synchronized(self)`**这样一个运行时特性来锁定对象,这么做对不对?对;是不是最合理的方式?不一定。 | |
往上说,在资源上加的任何锁都会造成性能损失,一方面,获取锁和释放锁带来额外的系统开销;另一方面,当一个任务获取锁后需要执行一段耗时的操作,其他线程可能因此等待锁被释放而造成资源浪费。 | |
往下说,iOS在`@synchronized`锁的实现上,使用了最多三个加/解锁序列,它在获取和释放锁时,相对效率要低一些。在需要频繁进出临界区域的应用场景下,我们应该保证只锁住尽可能少的必要的代码片段,提高效率。 | |
因此,`@synchronized`适用于低频获取/释放锁,且获取锁后执行代码片段较少的场景。 | |
> **Tip: ** 不过,由于它添加了异常开锁机制,当我们对代码在多线程上工作情况的预估和控制较弱,但又需要确保不会发生死锁的情况下,最简单的解决方式就是使用`@synchronized`。 | |
**而对于`@synchronized`衍生出来的两个问题,前者**,永远先从设计上去考虑是否合理,我们是否真的需要在不同的线程间访问同一个对象,尽量减少不同线程间的资源共享。当需求确实存在时,思考两个问题,第一,多个线程对于资源的访问是否存在时序上的先后关系,是否必须并发执行。如果可以先后顺序执行,把它放到一个串行队列中执行,可以免去对象加解锁开销;第二,是否必须使用非线程安全对象,能不能用线程安全对象替换。 | |
此外,考虑使用更高层次的保护替代低层次的保护。例如,类A中使用到了类B,类B中访问了对象C,如果类A能保证在任何时刻只有一个线程使用类B,那么类B就不需要再考虑对对象C的线程安全问题。 | |
**衍生出来的第二个问题**,为了锁住尽可能少的代码,我们可以使用GCD的隔离队列来替代锁的使用。即在同一时间只允许一个线程修改对象,但是允许多个线程读取对象。为了实现这一点,我们可以创建一个并发队列,使用`dispatch_barrier_async`分发写任务,在写操作时阻塞队列中其他任务;使用`dispatch_sync`分发读任务,同步获取读取结果。示例代码如下: | |
``` | |
// 创建隔离队列 | |
self.isolatedQueue = dispatch_queue_create("com.htao.isolated.testClass", DISPATCH_QUEUE_CONCURRENT); | |
// 写操作 | |
- (void)addRegisterValue:(NSString *)value forKey:(NSString *)key { | |
key = [key copy]; | |
dispatch_barrier_async(self.isolatedQueue, ^(){ | |
self.registerList[key] = value; | |
}); | |
} | |
// 读操作 | |
- (NSString *)getRegisterKeyString { | |
dispatch_sync(self.isolatedQueue, ^{ | |
[self.registerList enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { | |
// 遍历处理 | |
...... | |
}]; | |
}); | |
...... | |
} | |
``` | |
> **Tip:** 注意,这种做法只在需要高频加解锁的场景下,例如负责处理缓存的类。在客户端大部分场景下,是不需要被用到的,此时推荐**首选`@synchronized`**。 | |
总结 | |
------------- | |
看到这里,你是否对并发编程中的资源共享问题有了一定了解,但是面对多种多样的解决方案,又有一点难以权衡。不要紧,让我们一起总结下: | |
1. 从设计上思考合理性,减少多线程间的资源共享。 | |
2. 用线程安全对象替代非线程安全对象。 | |
3. 交给串行队列,保证同一时间只有一个线程访问对象。 | |
4. 使用高层级的保护替代底层级的保护。 | |
5. 使用`@synchronized`保护对象。 | |
6. 在频繁使用的场景下,使用隔离队列访问对象。 | |
> 发现了没,1、2、3、4其实都是让你避免锁的使用。 | |
回到最初的Crash | |
------------- | |
现在,回看一下最初发生crash的代码。 | |
``` | |
// 在interface中定义的属性 | |
@property(nonatomic, strong) NSMutableDictionary *registerList; | |
// 发生crash的语句 | |
NSString *requestKeyset = [TaffySerializeUtil toJSONString:self.registerList]; | |
``` | |
你能想出多少种解决方法?最合理的方案是什么? | |
[1]: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html#//apple_ref/doc/uid/10000057i-CH12-SW1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment