当我们在聊iOS中的多线程时,我们主要聊哪些问题?
多线程的方案有以下几种,分别是c语言的pthread、GCD以及基于这两者进行面向对象封装的NSThread、NSOperation。日常开发中使用比较多的当然是GCD了,不用手动管理线程的生命周期,可以充分利用设备的多核优势,以block的方式来执行任务这些都是GCD的优点。
GCD
使用GCD要注意几个概念:
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
- 任务:就是开启GCD时要执行的block里的代码
- 队列:把任务block放到哪个队列queue执行,队列的执行方式分为串行执行(DISPATCH_QUEUE_SERIAL)和并发执行(DISPATCH_QUEUE_CONCURRENT)
- sync or async:同步函数或者异步函数,两者最大的区别在于会不会开启新的线程,使用sync函数时,不会开启新的线程,并且加入到队列里的任务会立刻同步执行,换句话说就是会等block的返回,async函数有开启新线程的可能,但具体还要取决于执行任务的队列。
几种队列和函数结合情况:
死锁
上经典案例:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}
这段代码在打印完"1"之后crash了,原因是使用同步函数往主队列里添加任务,会导致主队列卡住,也就是产生了死锁。
具体来说就是主队列在执行viewDidLoad任务的过程中,往主队列中添加了"2"的任务,因为使用的是同步函数sync,sync函数有个特点就是添加到队列里的任务会立刻执行并等待返回,所以就要求"2"这个任务要立刻在主队列里执行,而主队列是个串行队列,需要执行完当前的viewDIdLoad任务才能往下执行,这就造成了两个任务在互相等待对方的执行完成,形成了死锁。
对上面的代码稍微做一下修改:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1");
dispatch_sync(dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL), ^{
NSLog(@"2");
});
NSLog(@"3");
}
还是用的同步函数sync,队列也是串行队列,但却没有crash,打印的顺序的是123,这是因为任务"2"是添加到了新创建的串行队列里,主线程遇到sync就立刻执行任务"2"了,执行完再继续执行主队列中的任务。
so,可以总结一下产生死锁的原因了:使用sync函数往当前串行队列中添加任务,就会产生死锁。
线程安全
在使用多线程的便利时,也可能会引起线程安全的问题,比如多个线程同时对同一块资源的访问读写行为。
保证线程同步的方案有很多种,上锁、用GCD的串行队列SerialQueue,设置最大线程并发数量dispatch_semaphore等方式都可以。
锁
- 自旋锁:等待锁的线程会处于忙等状态
- 互斥锁:等待锁的线程会处于休眠状态,等待唤醒
按创建锁的方式来分:
- OSSpinLock:自旋锁,在iOS10上过期了('OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from
instead) - os_unfair_lock:用来替代OSSpinLock的方案
- pthread_mutex_t:互斥锁,基于C语言的,可以跨平台使用
- NSLock:对mutex互斥锁的面相对象封装,使用简单
- @synchronized:是对mutex递归锁的封装,支持递归的方式进行加锁
不管是用那种方式创建锁,基本都离不开初始化锁、加锁、解锁这几个步骤,使用上也是大同小异。拿NSLock举个栗子:
NSLock *aLock = [[NSLock alloc] init];
[aLock lock];
// 加锁的代码
[aLock unlock];
有些时候我们是希望读取资源可以允许多条线程并发执行,而写入资源只允许一条线程去执行,并且读写是不能同时进行的,这种多读单写的操作可以用栅栏函数(dispatch_barrier_sync)或者读写锁(pthread_rwlock_t)来处理。
使用栅栏函数有个要注意的点,用来处理任务的队列必须是自己创建的并发队列,添加到栅栏函数队列里的任务,会在处理这个任务时形成一道栅栏,同一时间只允许线程去处理这个任务,不能用系统提供的全局并发队列,不然就失去栅栏的特性了。
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_barrier_async(queue, ^{
// do something
});
Runloop
每条线程都会有一个runloop与之对应,以线程作为key,runloop作为value存储在全局的字典里。主线程的runloop默认是开启的,在UIApplicationMain函数里获取并开启,子线程的runloop是没有开启的,在获取时会创建子线程的runloop,类似懒加载。
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
[thread start];
上面这条线程在处理完block里的任务后就会销毁了,如果有这么一个需求,想固定用一个子线程去处理事情,而不是每次都要重新创建一条新的线程,这就需要用到runloop来使线程保活了。
要想runloop一直运行不退出,就要确保runloop在当下的运行模式中有Source、Timer或者Observer,下面手动添加一个Source使runloop不退出。
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}];
[thread start];
这样这个runloop不会退出,线程是保活了,但也会引起另外的问题,看看苹果对run方法的描述,run方法会反复去调用[runMode: beforeDate:]这个方法,这会开启一个永不销毁的runloop,导致线程也销毁不了,也不是我们想要的,一般来说是想控制runloop在某个时机退出的,而且苹果也举了一个怎样退出runloop的栗子来作参考。
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
通过调用[runMode: beforeDate:]方法来启动runloop,并且在合适的时机把shouldKeepRunning设置为NO,这样调用停掉runloop的方法CFRunLoopStop就可以退出当前的runloop从而使线程不用时销毁。
参考资料:
GCD源码: https://github.com/apple/swift-corelibs-libdispatch
逆向OC类的开源代码:http://www.gnustep.org/resources/downloads.php
CFRunLoopRef:https://opensource.apple.com/tarballs/CF/