iOS中的常见多线程方案:
一、GCD的常用函数
GCD中有2个用来执行任务的函数:
用同步的方式执行任务(当前线程)
1
2
3// queue:队列
// block:任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);用异步的方式执行任务(子线程)
1
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
GCD源码:https://github.com/apple/swift-corelibs-libdispatch
GCD的队列可以分为2大类型:
并发队列(Concurrent Dispatch Queue)
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步(
dispatch_async
)函数下才有效
串行队列(Serial Dispatch Queue)
- 让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
有4个术语比较容易混淆:同步、异步、并发、串行。
同步和异步主要影响:能不能开启新的线程
- 同步:在当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
并发和串行主要影响:任务的执行方式
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,再执行下一个任务
dispatch_async
和dispatch_sync
用来控制是否要开启新的线程。队列(dispatch_queue
)的类型决定了任务的执行方式是并发还是串行,主队列也是一个串行队列。
二、死锁(经典面试题)
使用sync
函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)。
2.1. 示例代码一
1 | - (void)viewDidLoad { |
会产生死锁。输出顺序:1
,然后到线程1会卡死,不会输出2和3。
由于队列是先入先出的,viewDidLoad
的执行优先级比dispatch_sync
高,所以需要等待viewDidLoad
方法执行结束后才能执行同步线程中的任务。但由于是同步串行任务,因此会等待主线程的任务执行结束后(执行viewDidLoad
)再往后执行。此时,viewDidLoad
和dispatch_sync
会形成相互等待的状态(也就是死锁),最终导致程序崩溃。
2.2. 示例代码二
在示例代码一的基础上把dispatch_sync
修改为dispatch_async
。
1 | - (void)viewDidLoad { |
不会产生死锁。输出顺序:1 - 3 - 2
。
dispatch_async
不要求立马在当前线程同步执行任务。线程是添加到主队列中执行的,虽然是异步任务,但不会开启新的线程。
2.3. 示例代码三
在示例代码二的基础上把队列修改为创建的串行队列,并且在异步线程中向队列中添加同步线程。
1 | - (void)viewDidLoad { |
会产生死锁。输出顺序:1 - 5 - 2
,然后到线程2会卡死,不会输出3和4。
和示例代码一的原因一样,线程1和线程2都添加到同一个串行队列中,线程1执行结束的前提条件是线程2的任务已经执行结束,线程2执行结束的前提条件是线程1的任务已经执行结束,所以会处在相互等待的状态(死锁)。
2.4. 示例代码四
在示例代码三的基础上把线程2添加到创建的并发队列中。
1 | - (void)viewDidLoad { |
不会产生死锁。输出顺序:1 - 5 - 2 - 3 - 4
。
因为创建了2个不同的队列,当前每一个队列都只有一个任务,并且都是独立执行任务的,所以不会产生死锁。
2.5. 示例代码五
在示例代码四的基础上把线程2添加到创建的串行队列中。
1 | - (void)viewDidLoad { |
不会产生死锁。输出顺序:1 - 5 - 2 - 3 - 4
。
道理同示例代码四,两个不同的队列单独执行任务。
2.6. 示例代码六
在示例代码四的基础上把线程1添加也到并发队列中。
1 | - (void)viewDidLoad { |
不会产生死锁。输出顺序:1 - 5 - 2 - 3 - 4
。
因为是并发队列,所以线程1和线程2会独立执行。
三、队列组
思考:如何用GCD实现以下功能。
异步并发执行任务1、任务2。等任务1、任务2都执行完毕后,再回到主线程执行任务3。
1 | // 创建队列组 |
面试题1:下面代码执行结果是什么?
1 | - (void)viewDidLoad { |
输出:1 - 2
,不会输出3。
如果把afterDelay
取消,使用performSelector:withObject:
就会输出1 - 2 - 3
。
如果把任务放到主队列中,会输出1 - 3 - 2
。
performSelector:withObject:afterDelay
是RunLoop的一个扩展方法,它的本质是往RunLoop中添加定时器,但子线程默认是没有启动RunLoop的,所以上面的代码不会执行test
方法。要想解决这个问题,只需要启动RunLoop就可以。
1 | dispatch_async(queue, ^{ |
面试题2:下面代码执行结果是什么?
1 | - (void)viewDidLoad { |
输出结果是:1
,并且程序会崩溃。
由于block
和test
是在同一个线程上执行,并且会优先执行block中的任务,但是执行完block中的任务后线程就死掉了,所以再执行test
方法时会报错。
正确做法(线程保活):
1 | NSThread *thread = [[NSThread alloc] initWithBlock:^{ |
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍(Foundation等一些框架是不开源的),虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值。