多线程之GCD
一、GCD 核心概念
Grand Central Dispatch。它是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。同时它使用的也是 c语言,不过由于使用了 Block(Swift里叫做闭包),使得使用起来更加方便,而且灵活。所以基本上大家都使用 GCD 这套方案,老少咸宜,实在是居家旅行、杀人灭口,必备良药。
-
GCD的优势
-
GCD是苹果公司为多核的并行运算提出的解决方案
-
GCD会自动利用更多的CPU内核(比如双核、四核)
-
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
-
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
-
GCD中有2个核心概念
-
任务:执行什么操作,任务使用 block 封装
-
队列:用来存放(调度)任务
-
GCD的使用就2个步骤
-
定制任务
-
确定想做的事情
-
将任务添加到队列中
-
GCD会自动将队列中的任务取出,放到对应的线程中执行
-
任务的取出遵循队列的FIFO原则:先进先出,后进后出
- 执行任务的函数
- 异步 dispatch_async
1.不用等待当前语句执行完毕,就可以执行下一条语句
2.会开启线程执行 block 的任务
3.异步是多线程的代名词
- 同步 dispatch_sync
1.必须等待当前语句执行完毕,才会执行下一条语句
2.不会开启线程
3.在当前执行 block 的任务
-
同步和异步的区别
-
同步:不开线程,必须等待当前语句执行完毕,才会执行下一条语句
-
异步:开线程,不用等待当前语句执行完毕,就可以执行下一条语句
-
队列 - 负责调度任务
-
串行队列
1.一次只能"调度"一个任务
2.dispatch_queue_create("abc", NULL);
- 并发队列
1.一次可以"调度"多个任务
2.dispatch_queue_create("abc", DISPATCH_QUEUE_CONCURRENT);
- 主队列
1.专门用来在主线程上调度任务的队列
2.不会开启线程
3.在主线程空闲时才会调度队列中的任务在主线程执行
4.dispatch_get_main_queue()
- 队列的选择
- 多线程的目的:将耗时的操作放在后台执行!
- 串行队列,只开一条线程,所有任务顺序执行
如果任务有先后执行顺序的要求
效率低 -> 执行慢-> "省电"
有的时候,用户其实不希望太快!例如使用 3G 流量,"省钱"
- 并发队列,会开启多条线程,所有任务不按照顺序执行
如果任务没有先后执行顺序的要求
效率高 -> 执行快 -> "费电"
WIFI,包月
小结:
- 开不开线程由执行任务的函数决定
异步开,异步是多线程的代名词
同步不开 - (异步)开几条线程由队列决定
○ 串行队列开一条线程
○ 并发队列开多条线程,具体能开的线程数量由底层线程池决定
iOS 8.0 之后,GCD 能够开启非常多的线程
iOS 7.0 以及之前,GCD 通常只会开启 5~6 条线程
二.队列详解
1、串行队列
特点
以先进先出的方式,顺序调度队列中的任务执行。
无论队列中所指定的执行任务函数是同步还是异步,都会等待前一个任务执行完成后,再调度后面的任务。
创建
dispatch_queue_tqueue=dispatch_queue_create("abc",DISPATCH_QUEUE_SERIAL);
dispatch_queue_tqueue = dispatch_queue_create("abc", NULL);
1、串行队列
同步执行
/*
提问:是否开线程?是否顺序执行?come here 的位置?
结果:同步不开线程
*/
- (void)gcdDemo1{
//创建串行队列
dispatch_queue_tqueue = dispatch_queue_create("itcast.cn", DISPATCH_QUEUE_SERIAL);
//同步执行任务
for (int index = 0; index < 10; index ++) {
dispatch_sync(queue, ^{
NSLog(@"拼命下载中.....%@,i = %d",[NSThread currentThread],index);
});
}
NSLog(@"come here");
}
2、串行队列 异步执行
/**
提问:是否开线程?是否顺序执行?come here 的位置?
结果: 会开线程,顺序执行
*/
-(void)gcdDemo2{
//创建串行队列
dispatch_queue_tqueue = dispatch_queue_create("itcast.cn", NULL);
//异步执行任务
for (int index = 0; index < 10; index ++) {
dispatch_async(queue, ^{
NSLog(@"拼命下载中.....%@,i = %d",[NSThread currentThread],index);
});
}
NSLog(@"come here");
}
2、并发队列
特点
以先进先出的方式,并发调度队列中的任务执行。
如果当前调度的任务是同步执行的,会等待任务执行完成后,再调度后续的任务。
如果当前调度的任务是异步执行的,同时底层线程池有可用的线程资源,会再新的线程调度后续任务的执行。
创建
dispatch_queue_t queue = dispatch_queue_create("cn.itcast", DISPATCH_QUEUE_CONCURRENT);
/**
开不开线程由执行任务的函数决定
— 同步不开
- 异步开
(异步)开几条线程由队列决定
- 串行队列,只开一条
- 并发队列,开多条,由底层线程池决定
在iOS7.0之前,GCD底层线程池一般最多会开5到6条。
在iOS8.0之后,GCD底层线程池要多少给多少。
*/
/**
* 并发行队列异步执行
* 问:会不会开线程? 开几条线程? 顺序执行?
结果:会开,开多条(由底层线程池决定) 并发
*/
- (void)gcdDemo2 {
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("itheima", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; ++i) {
// 将任何添加到队列中
dispatch_async(queue, ^ {
NSLog(@"%@--%d",[NSThread currentThread],i);
});
}
NSLog(@"over");
}
/**
* 并发队列同步执行
问:会开线程吗?over是在后面还是前面输出?
猜:不会 后面输出
全中
*/
- (void)gcdDemo1 {
// 创建串行队列
dispatch_queue_t queue = dispatch_queue_create("itheima", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; ++i) {
// 将任务添加到队列中--同步执行
dispatch_sync(queue, ^ {
NSLog(@"%@--%d",[NSThread currentThread],i);
});
}
NSLog(@"over");
}
3、主队列
特点
Ø 专门用来在主线程上调度任务的队列。
Ø 不会开启线程。
Ø 以先进先出的方式,在主线程空闲时才会调度队列中的任务在主线程执行。
Ø 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度。
主队列的获取
Ø 主队列是负责在主线程调度任务的
Ø 会随着程序启动一起创建
Ø 主队列只需要获取不用创建
dispatch_queue_t queue = dispatch_get_main_queue();
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent(UIEvent *)event {
[self gcdDemo1];
NSLog(@"睡");
[NSThreadsleepForTimeInterval:1.0];
NSLog(@"醒");
}
1、主队列,异步执行
- (void)gcdDemo1{
dispatch_queue_tqueue = dispatch_get_main_queue();
for (int i =0; i < 10;++i) {
dispatch_async(queue, ^{
NSLog(@"%@ - %d", [NSThread currentThread], i);
});
NSLog(@"--->%d", i);
}
NSLog(@"come here");
}
提示:在主线程空闲时才会调度队列中的任务在主线程执行
2、主队列,同步执行
- (void)gcdDemo2{
//获得主队列
dispatch_queue_tqueue = dispatch_get_main_queue();
NSLog(@"!!!");
//同步执行任务
dispatch_sync(queue, ^{
NSLog(@"%@", [NSThreadcurrentThread]);
});
NSLog(@"come here");
}
**主队列和主线程相互等待会造成死锁**
3、主队列调度同步执行不死锁,把主队列的任务丢到子线程
- (void)gcdDemo3 {
dispatch_queue_tqueue = dispatch_queue_create("com.itheima.queue",DISPATCH_QUEUE_CONCURRENT);
void (^task)() = ^ {
NSLog(@"%@===>",[NSThreadcurrentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"死?");
});
};
dispatch_async(queue, task);
}
主队列在主线程空闲时才会调度队列中的任务在主线程执行
4、同步任务的作用
同步任务,可以让其他异步执行的任务,依赖某一个同步任务
例如:在用户登录之后,再异步下载文件!
- (void)gcdDemo1 {
dispatch_queue_tqueue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_CONCURRENT);
//所有的耗时操作都要异步执行
//同步任务不执行完,队列不会调度后续的任务
dispatch_sync(queue, ^{
NSLog(@"登录 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载 A %@",
[NSThreadcurrentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载 B %@",
[NSThreadcurrentThread]);
});
}
代码调整,让登录也异步执行
- (void)gcdDemo2 {
dispatch_queue_tqueue = dispatch_queue_create("com.itheima.queue", DISPATCH_QUEUE_CONCURRENT);
void (^task)() = ^{
dispatch_sync(queue, ^{
NSLog(@"登录 %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载 A %@", [NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"下载 B %@", [NSThread currentThread]);
});
};
dispatch_async(queue, task);
}
5、全局队列
全局队列是系统为了方便程序员开发提供的,其工作表现与并发队列一致。
全局队列 异步任务
/**
提问:是否开线程?是否顺序执行?come here的位置?
*/
- (void)gcdDemo8 {
// 1. 队列
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
// 2. 执行任务
for (inti = 0; i < 10; ++i) {
dispatch_async(q, ^{
NSLog(@"%@ - %d", [NSThread currentThread], i);
});
}
NSLog(@"come here");
}
运行效果与并发队列相同
全局队列和并发队列的比较
全局队列
没有名称
无论 MRC & ARC
都不需要考虑释放
日常开发中,建议使用"全局队列"
并发队列
有名字,和 NSThread 的 name 属性作用类似
如果在 MRC开发时,需要使用 dispatch_release(q); 释放相应的对象
dispatch_barrier 必须使用自定义的并发队列
开发第三方框架时,建议使用并发队列
dispatch_get_global_queue函数参数
1.服务质量(队列对任务调度的优先级)/iOS 7.0 之前,是优先级
iOS 8.0(新增)
QOS_CLASS_USER_INTERACTIVE 0x21, 用户交互(希望最快完成-不能用太耗时的操作)
QOS_CLASS_USER_INITIATED 0x19, 用户期望(希望快,也不能太耗时)
QOS_CLASS_DEFAULT 0x15, 默认(用来底层重置队列使用的,不是给程序员用的)
QOS_CLASS_UTILITY 0x11, 实用工具(专门用来处理耗时操作!)
QOS_CLASS_BACKGROUND 0x09, 后台
QOS_CLASS_UNSPECIFIED 0x00, 未指定,可以和iOS 7.0 适配
iOS 7.0
DISPATCH_QUEUE_PRIORITY_HIGH 2 高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默认优先级
DISPATCH_QUEUE_PRIORITY_LOW (-2) 低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 后台优先级
2.为未来保留使用的,应该永远传入0
结论:如果要适配 iOS 7.0 & 8.0,使用以下代码: dispatch_get_global_queue(0,
0);
6、Barrier异步
注意:dispatch_barrier_async 必须使用自定义队列,否则执行效果和全局队列一致
Ø 主要用于在多个异步操作完成之后,统一对非线程安全的对象进行更新
Ø 适合于大规模的 I/O 操作
准备工作
@interface ViewController (){
//加载照片队列
dispatch_queue_t _photoQueue;
}
@property (nonatomic, strong) NSMutableArray *photoList;
@end
@implementation ViewController
- (NSMutableArray *)photoList {
if (_photoList == nil) {
_photoList = [[NSMutableArray alloc] init];
}
return_photoList;
}
提示:NSMutableArray 是非线程安全的。
viewDidLoad
- (void)viewDidLoad {
[superviewDidLoad];
_photoQueue= dispatch_queue_create("com.itheima.com", DISPATCH_QUEUE_CONCURRENT);
for (int i =0; i < 330;++i) {
[self loadPhotos:i];
}
NSLog(@"come here");
}
模拟下载照片并在完成后添加到数组
- (void)loadPhotos:(int)index
{
dispatch_async(_photoQueue, ^{
NSString *fileName = [NSString stringWithFormat:@"%d.jpg", index %9];
NSString *path = [[NSBundlemainBundle] pathForResource:fileNameofType:nil];
UIImage *image = [UIImage imageWithContentsOfFile:path];
NSLog(@"下载照片%@,%d", fileName,index);
[self.photoList addObject:image];
});
}
由于 NSMutableArray 是非线程安全的,如果出现两个线程在同一时间向数组中添加对象,会出现程序崩溃的情况。
使用dispatch_barrier_async修复崩溃情况
- (void)loadPhotos:(int)index
{
dispatch_async(_photoQueue, ^{
NSString *fileName = [NSString stringWithFormat:@"%d.jpg", index %9];
NSString *path = [[NSBundle mainBundle] pathForResource:fileNameofType:nil];
UIImage *image = [UIImage imageWithContentsOfFile:path];
NSLog(@"下载照片%@,%d", fileName,index);
dispatch_barrier_async(_photoQueue, ^{
NSLog(@"添加图片 %@,%@", fileName,[NSThread currentThread]);
[self.photoList addObject:image];
});
});
}
使用 dispatch_barrier_async 添加的 block
会在之前添加的 block 全部运行结束之后,统一在同一个线程顺序执行,从而保证对非线程安全的对象进行正确的操作!
Barrier工作示意图
Snip20160427_7.png
7.一次性执行
有的时候,在程序开发中,有些代码只想从程序启动就只执行一次,典型的应用场景就是“单例”。
// MARK: 一次性执行
- (void)once {
static dispatch_once_t onceToken;
NSLog(@"%ld",onceToken);
dispatch_once(&onceToken, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"一次性吗?");
});
NSLog(@"come here");
}
dispatch
内部也有一把锁,是能够保证"线程安全"的!而且是苹果公司推荐使用的。
以下代码用于测试多线程的一次性执行
- (void)demoOnce {
for (int i =0; i < 10;++i) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self once];
});
}
}
8.延迟操作
#pragma mark - 延迟执行
- (void)delay {
/**
从现在开始,经过多少纳秒,由"队列"调度 异步执行block 中的代码
参数
1. when 从现在开始,经过多少纳秒
2. queue 队列
3. block 异步执行的任务
*/
dispatch_time_twhen = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0* NSEC_PER_SEC));
void (^task)() = ^ {
NSLog(@"%@", [NSThreadcurrentThread]);
};
//主队列
dispatch_after(when, dispatch_get_main_queue(), task);
//全局队列
// dispatch_after(when, dispatch_get_global_queue(0, 0), task);
//串行队列
// dispatch_after(when, dispatch_queue_create("itheima", NULL),
task);
NSLog(@"come here");
}
三.调度组
dispatch_group_async可以实现监听一组任务是否完成,完成后得到通知执行其他的操作。
比如你执行三个下载任务,当三个任务都下载完成后你才通知界面说完成的了
- (void)group1 {
//1. 调度组
dispatch_group_tgroup = dispatch_group_create();
//2. 队列
dispatch_queue_tq = dispatch_get_global_queue(0, 0);
//3. 将任务添加到队列和调度组
dispatch_group_async(group, q, ^{
[NSThread sleepForTimeInterval:1.0];
NSLog(@"任务 1 %@",[NSThreadcurrentThread]);
});
dispatch_group_async(group, q, ^{
NSLog(@"任务 2 %@",[NSThreadcurrentThread]);
});
dispatch_group_async(group, q, ^{
NSLog(@"任务 3 %@",[NSThreadcurrentThread]);
});
//4. 监听所有任务完成
dispatch_group_notify(group, q, ^{
NSLog(@"OVER %@", [NSThreadcurrentThread]);
});
//5. 判断异步
NSLog(@"come here");
}
#pragma mark - dispatch_group_async 的实现原理
终端输入命令:man dispatch_group_async
The dispatch_group_async() convenience function behaves like so:
void
dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block)
{
dispatch_retain(group);
dispatch_group_enter(group);
dispatch_async(queue, ^{
block();
dispatch_group_leave(group);
dispatch_release(group);
});
}
注意:进群组和离开群组一定要成对出现