Skip to content

Instantly share code, notes, and snippets.

@xiayun200825
Created June 29, 2016 03:11
Show Gist options
  • Save xiayun200825/c5949dceb7b283d157ad3ab9c3c4766c to your computer and use it in GitHub Desktop.
Save xiayun200825/c5949dceb7b283d157ad3ab9c3c4766c to your computer and use it in GitHub Desktop.
iOS并发编程最佳实践之资源共享
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