OC
中常见的多线程方案有以下几种:
技术方案 | 简介 | 语言 | 生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 1:一套通用的多线程API 2:适用于Unix \ Linux \ Windows等操作系统 3: 跨平台可移植 4: 使用难度大 |
C | 程序员管理 | 几乎不用 |
NSSThread | 1:使用更加面向对象 2:可直接操作线程对象 |
OC | 程序员管理 | 偶尔使用 |
GCD | 1:旨在替换 NSThread 2:充分利用设备的多核 |
C | 自动管理 | 经常使用 |
NSOperation | 1:基于GCD (底层是GCD) 2:比GCD多了一些更简单使用的功能 3:使用更加面向对象 |
OC | 自动管理 | 经常使用 |
GCD
在我们项目中用的比较多,所以今天主要研究一下GCD
.
GCD
有两个用来执行任务的函数:
-
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
同步执行- 立马在
当前线程
执行,要求当前任务执行完才能执行下一个任务 - 不具备开启新线程的能力
- 立马在
-
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
异步执行- 不要求立马执行,可以等一会儿再执行.
- 具备开启新线程的能力.
GCD
中还有两个非常重要的概念任务
和队列
.
而队列可以分为两大类型:
- 串行队列
- 让任务一个一个执行,要求上一个任务执行完才能执行下一个任务.
- 并发队列
- 可以同时执行多个任务 (自动开启多个线程同时执行任务).
- 并发队列只有在异步函数下才有效.
下面我们就用代码来验证一下上面的几个概念,加深印象.
如上图所示,我们虽然吧两个任务添加到了全局并发队列中,但是执行的时候并不是同时执行的,而是等任务一执行完毕之后才执行任务二.这就是因为
sync
不具备开启新线程的能力,即使是并发队列也得在同一个线程中按顺序执行.
如果想让任务一和任务二同时执行,就需要在异步函数中执行任务:
需要注意的是,
如果往主队列中添加任务,即使在异步函数也不会创建新的线程
:
这是因为主队列是一种特殊的串行队列,主队列中的任务会在主线程中执行
同步 , 异步 , 串行 , 并发
这几个概念容易混淆,通过表格捋清楚他们的区别:
并发队列 | 手动创建的串行队列 | 主队列 | |
---|---|---|---|
同步 (sync) | 没有 开启新线程 串行 执行任务 |
没有 开启新线程 串行 执行任务 |
没有 开启新线程 串行 执行任务 |
异步 (async) | 有 开启新线程 并发 执行任务 |
有 开启新线程 串行 执行任务 |
没有 开启新线程 串行 执行任务 |
在同步 (sync)
函数下,不管是串行队列,并发队列还是主队列,统统不会开启新线程,都是串行执行任务;而即使在异步 (async)
函数下,如果是主队列
还是不会开启新线程并且串行执行任务.
练习一:以下代码的执行结果:
//以下代码在主线程环境下执行
- (void)viewDidLoad{
NSLog(@"执行任务1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
以上代码在打印完执行任务1
后就崩溃了,因为产生了死锁.产生死锁的原因很简单:因为viewDidLoad
是在主线程中执行的,然后又添加了一个任务2到主队列中,并且是同步执行的.我们知道主队列中的任务也是在主线程中执行的.而主线程是串行执行任务的.现在立马要在当前线程 (主线程)中执行任务2,并且任务2执行完毕后才能继续往下执行 (因为是 sync).但是串行要求必须上一个任务执行完后才会执行下一个任务.也就是任务1和任务3都执行完毕后才会执行任务2.而任务3又再等到任务2执行完才能执行.任务2和任务3在互相等待,所以形成了死锁:
练习二:以下代码的执行结果:
以下代码在主线程环境下运行
- (void)viewDidLoad{
NSLog(@"执行任务1");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
}
//打印结果
2019-12-10 16:40:39.198263+0800 GCD多线程Test[16679:4055662] 执行任务1
2019-12-10 16:40:39.198445+0800 GCD多线程Test[16679:4055662] 执行任务3
2019-12-10 16:40:39.213941+0800 GCD多线程Test[16679:4055662] 执行任务2
这种是不会产生死锁的,因为虽然viewDidLoad
中的任务1和任务3是在主线程中执行的,并且任务2也加到了主线程中执行,但是这里调用的是async
函数,async
函数不会要求立马在当前线程同步执行,它可以等上一个任务执行完毕后再执行下一个任务.
练习三:以下代码的执行结果:
以下代码在主线程环境下运行
- (void)viewDidLoad{
NSLog(@"执行任务1 - %@",[NSThread currentThread]);
//串行队列
dispatch_queue_t queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
//异步执行
dispatch_async(queue, ^{
NSLog(@"执行任务2 - %@",[NSThread currentThread]);
//同步执行
dispatch_sync(queue, ^{
NSLog(@"执行任务3 - %@",[NSThread currentThread]);
});
NSLog(@"执行任务4 - %@",[NSThread currentThread]);
});
NSLog(@"执行任务5 - %@",[NSThread currentThread]);
}
以上代码会在执行任务1,任务5,任务2后产生死锁崩溃.原因如下:
任务1和任务5被添加到主队列,执行到
dispatch_async
会创建一个新的子线程,然后把任务2和任务4放入到串行队列.任务2执行完毕后,dispatch_sync
把任务3加入到串行队列,并且要求立马在当前线程执行完毕后才能继续往下执行.而任务3要想执行必须等到任务4执行完毕,任务4又在等待任务3执行完毕.最后任务3和任务4互相等待产生死锁.
练习四:以下代码的执行结果:
以下代码在主线程环境下运行
- (void)viewDidLoad{
NSLog(@"执行任务1 - %@",[NSThread currentThread]);
//串行队列
dispatch_queue_t queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
//串行队列
dispatch_queue_t queue2 = dispatch_queue_create("serial2", DISPATCH_QUEUE_SERIAL);
//异步执行
dispatch_async(queue, ^{
NSLog(@"执行任务2 - %@",[NSThread currentThread]);
//同步执行
dispatch_sync(queue2, ^{
NSLog(@"执行任务3 - %@",[NSThread currentThread]);
});
NSLog(@"执行任务4 - %@",[NSThread currentThread]);
});
NSLog(@"执行任务5 - %@",[NSThread currentThread]);
}
运行结果:
2019-12-10 17:10:20.164937+0800 GCD多线程Test[16792:4179650] 执行任务1 - {number = 1, name = main}
2019-12-10 17:10:20.165164+0800 GCD多线程Test[16792:4179650] 执行任务5 - {number = 1, name = main}
2019-12-10 17:10:20.165194+0800 GCD多线程Test[16792:4179826] 执行任务2 - {number = 3, name = (null)}
2019-12-10 17:10:20.165326+0800 GCD多线程Test[16792:4179826] 执行任务3 - {number = 3, name = (null)}
2019-12-10 17:10:20.165549+0800 GCD多线程Test[16792:4179826] 执行任务4 - {number = 3, name = (null)}
正常运行,并不会产生死锁,因为任务3被添加到了一个新的队列:
练习五:以下代码的执行结果:
以下代码在主线程环境下运行
- (void)interView05{
NSLog(@"执行任务1 - %@",[NSThread currentThread]);
//串行队列1
dispatch_queue_t queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
//串行队列2
dispatch_queue_t queue2 = dispatch_queue_create("concurrent", DISPATCH_QUEUE_SERIAL);
//异步执行
dispatch_async(queue, ^{
NSLog(@"执行任务2 - %@",[NSThread currentThread]);
//同步执行
dispatch_sync(queue2, ^{
NSLog(@"执行任务3 - %@",[NSThread currentThread]);
});
NSLog(@"执行任务4 - %@",[NSThread currentThread]);
});
NSLog(@"执行任务5 - %@",[NSThread currentThread]);
}
运行结果
2019-12-10 17:10:20.164937+0800 GCD多线程Test[16792:4179650] 执行任务1 - {number = 1, name = main}
2019-12-10 17:10:20.165164+0800 GCD多线程Test[16792:4179650] 执行任务5 - {number = 1, name = main}
2019-12-10 17:10:20.165194+0800 GCD多线程Test[16792:4179826] 执行任务2 - {number = 3, name = (null)}
2019-12-10 17:10:20.165326+0800 GCD多线程Test[16792:4179826] 执行任务3 - {number = 3, name = (null)}
2019-12-10 17:10:20.165549+0800 GCD多线程Test[16792:4179826] 执行任务4 - {number = 3, name = (null)}
任务1和任务5被放到主队列中执行,任务2和任务4被放到一个串行队列中执行.执行完任务2后,dispatch_sync
会把任务3加入到另一个串行队列,并且要求任务3执行完毕后才能继续往下执行任务4.这里不会产生死锁,因为任务2,3,4并不在同一个串行队列中,任务2和任务4在一个串行队列,任务3在另一个串行队列:
通过上面几个练习,我们现在来总结一下什么情况下才会产生死锁:
使用sync
往当前(同一个)串行队列中添加任务,会卡住当前的串行队列,产生死锁.这完全是因为sync
和串行队列
的特性导致的.sync
要求执行完当前任务才会继续往下走;串行队列
要求上一个任务执行完才能执行下一个任务.新任务等旧任务,旧任务等新任务,就产生了死锁.
练习六:以下代码的执行结果:
以下代码在主线程环境下运行
- (void)viewDidLoad{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"执行任务1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"执行任务2");
});
}
- (void)test{
NSLog(@"执行任务3");
}
运行结果:
2019-12-10 17:45:15.301413+0800 GCD多线程Test[16871:4314226] 执行任务1
2019-12-10 17:45:15.301663+0800 GCD多线程Test[16871:4314226] 执行任务2
运行发现只打印了任务1和任务2,任务3没有执行,这是为什么呢?我们把[self performSelector:@selector(test) withObject:nil afterDelay:.0];
换成[self performSelector:@selector(test) withObject:nil];
再运行一下:
2019-12-10 17:47:05.195349+0800 GCD多线程Test[16889:4327171] 执行任务1
2019-12-10 17:47:05.195508+0800 GCD多线程Test[16889:4327171] 执行任务3
2019-12-10 17:47:05.195640+0800 GCD多线程Test[16889:4327171] 执行任务2
这次三个任务都打印了,这是为什么呢?
我们再把队列和GCD
的异步函数先注释掉再运行一下:
以下代码在主线程环境下运行
- (void)viewDidLoad{
// dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// dispatch_async(queue, ^{
NSLog(@"执行任务1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"执行任务2");
// });
}
运行结果:
2019-12-10 18:00:56.106856+0800 GCD多线程Test[16945:4421406] 执行任务1
2019-12-10 18:00:56.107114+0800 GCD多线程Test[16945:4421406] 执行任务2
2019-12-10 18:00:56.124219+0800 GCD多线程Test[16945:4421406] 执行任务3
也能正常打印,很奇怪.难道这两个方法有什么区别吗?
我们看看- (id)performSelector:withObject
的底层:(打开runtime
源码 -> 找到NSObject.mm
类 -> 找到performSelector
方法)
- (id)performSelector:(SEL)sel withObject:(id)obj {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}
而我们在runtime
源码中怎么也找不到- (void)performSelector:withObject:afterDelay:
方法的实现.我们点击此方法,发现这个方法是在NSRunLoop.h
中声明的.
其实- (void)performSelector:withObject:afterDelay:
这句代码的本质就是向runloop
中添加一个定时器Timmer
,由runloop
去处理这个定时器事件.所以要想让这个方法执行,必须要有runloop
,而我们知道,主线程默认是有runloop
的,而子线程默认是没有runloop
的,所以任务3始终没有执行.要想让任务3执行,我们就要获取子线程的runloop
并且让其运行起来:
- (void)viewDidLoad{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"执行任务1");
//本质就是向 runloop 中添加 timmer
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"执行任务2");
//启动 runloop
[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1E30, false);
});
}
运行结果:
2019-12-10 18:19:31.455625+0800 GCD多线程Test[16990:4547398] 执行任务1
2019-12-10 18:19:31.455903+0800 GCD多线程Test[16990:4547398] 执行任务2
2019-12-10 18:19:31.456082+0800 GCD多线程Test[16990:4547398] 执行任务3
总结:在子线程中使用NSTimmer
,必须要开启runloop
.因为如果不开启runloop
,子线程执行完毕后就挂掉了,怎么可能再让子线程在几秒后执行其他任务呢?他都已经挂掉了.
练习七:以下代码的执行结果:
- (void)viewDidLoad{
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"执行任务1");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test{
NSLog(@"执行任务2");
}
运行结果
2019-12-10 18:58:40.907272+0800 GCD多线程Test[17500:4757270] 执行任务1
2019-12-10 18:58:40.926970+0800 GCD多线程Test[17500:4756973] *** Terminating
app due to uncaught exception 'NSDestinationInvalidException', reason: '***
-[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]:
target thread exited while waiting for the perform'
执行完任务1后就崩溃了.这也是同样的问题:执行[thread start]
,thread
就会执行block
中的任务1,执行完任务1后线程就挂掉了(退出了).这时再让thread
去performSelector
肯定会报错.
队列组
如果说现在有这么一个需求:异步执行任务1,任务2.等任务1和任务2都执行完毕后再回到主线程执行任务3.如果说用GCD
怎么实现呢?这就要用到任务组了:
- (void)viewDidLoad{
//创建一个并发队列
dispatch_queue_t queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
//创建一个队列组
dispatch_group_t group = dispatch_group_create();
//把任务1放到队列组中
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i ++) {
NSLog(@"执行任务 1 --- %@",[NSThread currentThread]);
}
});
//把任务2放到队列组中
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i ++) {
NSLog(@"执行任务 2 --- %@",[NSThread currentThread]);
}
});
//notify 等任务1和任务2执行完后再执行任务3
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i ++) {
NSLog(@"执行任务 3 --- %@",[NSThread currentThread]);
}
});
}
使用多线程的安全隐患 (线程安全
)
如果多个线程同时访问同一块数据资源,就很可能发生数据错乱和数据安全的问题.这是我们就要使用线程同步
技术.