深入浅出iOS多线程(一)——线程的概念
深入浅出iOS多线程(二)——pthraed和NSThread的使用
深入浅出iOS多线程(三)——GCD多线程
深入浅出iOS多线程(四)——NSOperation多线程
深入浅出iOS多线程(五)——多线程锁
GCD简介
前面分析的pthread和NSThread多线程技术,直接操作线程,而下面分析的GCD以及下一篇NSOpration都是并发解决技术
什么是GCD
- 全称是Grand Central Dispatch,可译为“宏大的中央调度”
- 纯C语言,提供了非常强大的函数
- GCD完全面向过程的并发解决技术
GCD的优势
- GCD是苹果公司为多核的并行运算提出的解决方案
- GCD会自动利用更多的CPU内核(比如双核、四核)
- GCD会自动管理多线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
GCD的两个核心概念
- 任务:执行什么操作
- 队列:用来存放任务
GCD的使用步骤
- 确定任务
- 将任务添加到队列中
- GCD会自动将队列的任务取出,放到对应的线程中执行
- 任务的取出遵循队列的FIFO原则,先进先出,后进后出
- 程序员要做的,只是将任务添加到队列,队列按照程序员指定的方式,调度任务执行任务的方法:同步和异步
- 同步:一个任务没有结束,就不会执行下一个任务
- 异步:不用等待任务执行完毕,就会执行下一个任务
GCD简单使用
同步GCD
-
同步执行方法,当前任务不执行完成,就不会执行下一个任务,同步执行不会开启线程
//1.创建队列 dispatch_queue_t g = dispatch_get_global_queue(0, 0); //2.将任务添加到队列中 //2.1.定义任务 void(^task)(void) = ^{ NSLog(@"%@",[NSThread currentThread]); }; //2.2 添加任务到队列,并且直接执行 dispatch_sync(g, task);
异步GCD
-
异步执行方法,如果任务没有执行完毕,可以不用等待,异步执行下一个任务,具备开启线程的能力,异步通常又是多线程的代名词
//1.创建队列 dispatch_queue_t g = dispatch_get_global_queue(0, 0); //2.将任务添加到队列中 //2.1.定义任务 void(^task)(void) = ^{ NSLog(@"%@",[NSThread currentThread]); }; //2.2 添加任务到队列,并且直接执行 dispatch_async(g, task);
GCD的线程通信(GCD主线程)
- 主队列,更新UI,专门负责在主线程上调度任务的队列
- 主线程中用同步跟异步都是一样的
dispatch_async(dispatch_get_global_queue(0, 0 ), ^{
NSLog(@"%@",[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"%@",[NSThread currentThread]);
});
});
GCD子线程下载图片,主线程更新UI
- 用GCD来异步下载图片,主线程中更新UI
- 简单逻辑来讲,用GCD更加简洁,看着就想一气呵成很爽
- 开发是要看线程的通信(线程中的业务逻辑是否过于复杂),如果过于复杂,建议使用@selector()的形式去使用。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSURL * url = [NSURL URLWithString:@"https://images.unsplash.com/photo-1496840220025-4cbde0b9df65?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2734&q=80"];
NSData * data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
[self.imageView sizeToFit];
self.scrollView.contentSize = image.size;
});
});
GCD队列
先把GCD异步、同步和队列、block的概念梳理一遍:
-
GCD 核心概念:将任务添加到队列,指定任务执行的方法
- 任务
- 使用block封装
- block就是一个提前准备的代码块,在需要的时候执行
- 队列(负责调度任务)
- 串行队列:一个接一个的调度任务
- 并发队列:可以同时调度多个任务
- 任务执行函数(任务都需要在线程中执行)
- 同步执行:当前指令不完成,不会执行下个指令
- 异步执行:当前指令不完成,不会等待,同样执行下个指令
- 任务
GCD串行队列
- 串行队列,同步任务
- 不会开启线程,顺序执行
- (void)gcdQueueDemo1{
//队列:串行
/**
1.队列名称
2.队列的属性 :DISPATCH_QUEUE_SERIAL == NULL 标示串行
**/
dispatch_queue_t q = dispatch_queue_create("struggle3g", NULL);
//2.同步执行任务
for (int i = 0; i<10; i++) {
dispatch_sync(q, ^{
NSLog(@"%@,%d",[NSThread currentThread],i);
});
}
}
- 串行队列,异步任务
- 不会开启线程,顺序执行
- (void)gcdQueueDemo2{
//队列:串行
/**
1.队列名称
2.队列的属性 :DISPATCH_QUEUE_SERIAL == NULL 标示串行
**/
dispatch_queue_t q = dispatch_queue_create("struggle3g", NULL);
//2.异步执行任务
for (int i = 0; i<10; i++) {
dispatch_async(q, ^{
NSLog(@"%@,%d",[NSThread currentThread],i);
});
}
}
- 总结:
- 会不会去线程池中拿线程是由任务类型来决定(异步or同步)
- 串行队列:肯定是先进先出,
- 当一个任务在执行的时候,下一个任务是没有执行的可能的
- 在串行队列中,每次只能执行一个任务,而不管你任务是异步还是同步,队列已经把下一个任务给封住了,只有串行队列中的任务完成了,才能执行下一个任务。
并行队列
-
并发队列,异步任务
- 会开线程,不会顺序执行
- (void)gcdQueueDemo3{ //队列:并发 /** 1.队列名称 2.队列的属性 :DISPATCH_QUEUE_CONCURRENT 并发队列 **/ dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT); //2.异步执行任务 for (int i = 0; i<10; i++) { dispatch_async(q, ^{ NSLog(@"%@,%d",[NSThread currentThread],i); }); } }
-
并发队列,同步任务
- 不会开线程,按顺序执行
- (void)gcdQueueDemo4{ //队列:并发 /** 1.队列名称 2.队列的属性 :DISPATCH_QUEUE_CONCURRENT 并发队列 **/ dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT); //2.异步执行任务 for (int i = 0; i<10; i++) { dispatch_sync(q, ^{ NSLog(@"%@,%d",[NSThread currentThread],i); }); } }
全局队列
全剧队列创建API
dispatch_queue_global_t
dispatch_get_global_queue(long identifier, unsigned long flags);
参数介绍:涉及到系统适配
-
iOS 8 服务质量
QOS_CLASS_USER_INTERACTIVE 用户交互(希望线程快速被执行,不要用好使的操作) QOS_CLASS_USER_INITIATED 用户需要的(同样不要使用耗时操作) QOS_CLASS_DEFAULT 默认的(给系统来重置队列的) QOS_CLASS_UTILITY 使用工具(用来做耗时操作) QOS_CLASS_BACKGROUND 后台 QOS_CLASS_UNSPECIFIED 没有指定优先级
-
iOS 7 调度的优先级
- DISPATCH_QUEUE_PRIORITY_HIGH 2 高优先级 - DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默认优先级 - DISPATCH_QUEUE_PRIORITY_LOW (-2) 低优先级 - DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 后台优先级
提示:尽可能不要选择BACKGROUND 优先级,服务质量,线程执行会很慢
- 第一个参数:identifier:iOS8给服务质量,iOS7是优先级
第二个参数:flags:为未来使用的一个保留值,现在始终给0
队列总结
开不开线程,取决于执行任务的函数,同步不开,异步才能开
开几条线程,取决于队列,串行开一条,并发可以开多条(异步)
-
全局队列 & 并发队列
- 1> 名称,并发队列取名字,适合于企业开发跟踪错误
- 2> release,在MRC 并发队列 需要使用的
- dispatch_release(q);//ARC 情况下不需要release !
-
全局队列 & 串行队列
- 全局队列: 并发,能够调度多个线程,执行效率高
- 费电 性能提高
串行队列:一个一个执行,执行效率低 - 省电 性能低
-
判断依据:用户上网方式
- WIFI : 可以多开线程
- 流量 : 尽量少开线程
GCD队列实际应用
在开发中,通常将耗时操作放入后台执行,有时候,有些任务彼此有依赖关系
-
例子:购买->登陆->支付,3种方式,代码如下
//一. 并发队列 同步任务 dispatch_queue_t buyqueue = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT); //1. 用户购买 dispatch_sync(buyqueue, ^{ NSLog(@"用户购买"); }); //2. 用户登陆 dispatch_sync(buyqueue, ^{ NSLog(@"用户登陆"); }); //3. 用户支付 dispatch_sync(buyqueue, ^{ NSLog(@"用户支付"); }); //二. 串行队列 异步任务 dispatch_queue_t buyqueue = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_SERIAL); //1. 用户购买 dispatch_async(buyqueue, ^{ NSLog(@"用户购买"); }); //2. 用户登陆 dispatch_async(buyqueue, ^{ NSLog(@"用户登陆"); }); //3. 用户支付 dispatch_async(buyqueue, ^{ NSLog(@"用户支付"); }); //三. 串行队列 同步任务 dispatch_queue_t buyqueue = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_SERIAL); //1. 用户购买 dispatch_sync(buyqueue, ^{ NSLog(@"用户购买"); }); //2. 用户登陆 dispatch_sync(buyqueue, ^{ NSLog(@"用户登陆"); }); //3. 用户支付 dispatch_sync(buyqueue, ^{ NSLog(@"用户支付"); });
例子:并发队列调度多个任务前,指定一个同步任务,让所有的异步任务,等待同步任务执行完成,这就是依赖关系
-
同步任务,会造成一个死锁!
dispatch_queue_t q = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT); //任务 void (^task)()=^{ for (int i = 0; i < 10; i++) { NSLog(@"%d %@",i ,[NSThread currentThread]); } //1.用户登录 dispatch_sync(q, ^{ NSLog(@"用户登录 %@",[NSThread currentThread]); }); //2.支付 dispatch_async(q, ^{ NSLog(@"支付 %@",[NSThread currentThread]); }); //3.下载 dispatch_async(q, ^{ NSLog(@"下载 %@",[NSThread currentThread]); }); }; dispatch_async(q, task);
GCD延迟执行(GCD定时器)
- 从现在开始,过了多少纳秒之后,让 queue队列,调度 block 任务,异步执行!
代码如下:
/**
参数:
1.dispatch_time_t
2.queue
3.block
*/
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.00003 * NSEC_PER_SEC));
dispatch_after(when, dispatch_queue_create("struggle3g", NULL), ^{
NSLog(@"%@",[NSThread currentThread]);
});
GCD一次执行
- 苹果提供的 一次执行机制,不仅能够保证一次执行!而且是线程安全的!!
- 更多的时候是用这种机制来做一个单例
//苹果提供的 一次执行机制,不仅能够保证一次执行!而且是线程安全的!!
static dispatch_once_t onceToken;
NSLog(@"%ld",onceToken);
//苹果推荐使用 gcd 一次执行,效率高
//不要使用互斥锁,效率低!
dispatch_once(&onceToken, ^{
//只会执行一次!!
NSLog(@"执行了%@",[NSThread currentThread]);
});
GCD调度组(Group)
- 创建队列
- 创建调度组
- 添加任务,让队列调度
- 所有线程任务执行完毕,通知
//1.队列
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
//2.调用组
dispatch_group_t g = dispatch_group_create();
//3.添加任务,让队列调度,任务执行情况,最后通知群组
dispatch_group_async(g, q, ^{
NSLog(@"downloadA %@",[NSThread currentThread]);
});
dispatch_group_async(g, q, ^{
[NSThread sleepForTimeInterval:5];
NSLog(@"downloadB %@",[NSThread currentThread]);
});
dispatch_group_async(g, q, ^{
NSLog(@"downloadC %@",[NSThread currentThread]);
});
//4.所有任务执行完毕,通知
//dispatch_group_notify 本身也是异步的
dispatch_group_notify(g, q, ^{
NSLog(@"OK %@",[NSThread currentThread]);
});
//如果想要dispatch_group_notify直接在主线程需要在其中传入主线程队列
dispatch_group_notify(g, dispatch_get_main_queue(), ^{
NSLog(@"OK %@",[NSThread currentThread]);
});
GCD主队列
dispatch_queue_t mainqueue = dispatch_get_main_queue();
主队列 & 串行队列的区别
都是 一个一个安排任务
队列特点:FIFO
并发队列 可以调度很多任务
-
串行独立, 必须等待一个任务执行完成,再调度另外一个
- 最多只能开启一条线程
-
主队列,以FIFO调度任务,如果主线程上有任务在执行,主队列就不会调度任务
- 主要是负责在主线程上执行任务
主队列同步任务
-
崩溃
- 主队列无法实现同步任务
NSLog(@"这里!!%@",[NSThread currentThread]); //1.队列 --> 已启动主线程,就可以获取主队列 dispatch_queue_t q = dispatch_get_main_queue(); //2.同步任务 dispatch_sync(q, ^{ NSLog(@"我要崩溃 %@",[NSThread currentThread]); }); NSLog(@"come here");
主线程异步任务
- 主队列是专门负责在主线程上调度任务的队列 --> 不会开线程
//1.队列 --> 已启动主线程,就可以获取主队列
dispatch_queue_t q = dispatch_get_main_queue();
//2.异步任务
dispatch_async(q, ^{
NSLog(@"%@",[NSThread currentThread]);
});
NSLog(@"come here");
主线程同步任务(不死锁)
void (^task)(void) = ^{
NSLog(@"这里!!%@",[NSThread currentThread]);
//1.队列 --> 已启动主线程,就可以获取主队列
dispatch_queue_t q = dispatch_get_main_queue();
//2.同步任务
dispatch_sync(q, ^{
NSLog(@"主线程 %@",[NSThread currentThread]);
});
NSLog(@"come here");
};
//开启一个线程
dispatch_async(dispatch_get_global_queue(0, 0), task);
GCD(Apply)重复调用
- 重复执行某个任务,但是注意这个方法没有办法异步执行(为了不阻塞线程可以使用dispatch_async()包装一下再执行)。
全局并发队列中的Apply
/*
全局并发队列
结果验证:并发队列,开启子线程,乱序执行
*/
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_apply(10, queue, ^(size_t count) {
NSLog(@"count = %zu,%@",count, [NSThread currentThread]);
});
/*
并发队列
结果验证:并发队列,开启子线程,乱序执行
*/
dispatch_queue_t queue = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(10, queue, ^(size_t count) {
NSLog(@"count = %zu,%@",count, [NSThread currentThread]);
});
/*
串行队列
结果验证:串行队列,按顺序执行
*/
dispatch_queue_t queue = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_SERIAL);
dispatch_apply(10, queue, ^(size_t count) {
NSLog(@"count = %zu,%@",count, [NSThread currentThread]);
});
注意主线程队列中的Apply
-
主线程中的Apply直接崩溃,在堆栈中我们可以看到的信息
__DISPATCH_WAIT_FOR_QUEUE__
- 调度等待队列
- 线程死锁,由于在主线程开启了一个同步任务,而主线程运行到
dispatch_sync
的时候,dispatch_sync中的内容需要让主线程其他任务等待它完成时才能运行,而主线程其他任务也是需要dispatch_sync
等待它们执行完成时才能调用,从而造成线程的死锁问题。
//主线程队列 同步任务 dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_sync(queue, ^{ NSLog(@"%@", [NSThread currentThread]); });
GCD(dispatch_barrier_async())
- 使用此方法创建的任务首先会查看队列中有没有别的任务要执行,如果有,则会等待已有任务执行完毕再执行;
- 同时在此方法后添加的任务必须等待此方法中任务执行后才能执行。
- 利用这个方法可以控制执行顺序,例如前面先加载最后一张图片的需求就可以先使用这个方法将最后一张图片加载的操作添加到队列,然后调用dispatch_async()添加其他图片加载任务
dispatch_barrier_async顾名思义就是栅栏函数,就是等于在一个并发队列中,在多个任务之间增加一道栅栏,当栅栏以及栅栏之前的任务完成以后还能执行,栅栏以后的代码。
```
dispatch_queue_t queue = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"++1++%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"++2++%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"++3++%@",[NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
NSLog(@"栅栏函数");
});
dispatch_async(queue, ^{
NSLog(@"++4++%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"++5++%@",[NSThread currentThread]);
});
```
输出结果一定是 1、2、3前面三个部分顺序执行,第四个一定是栅栏函数,后面2个顺序打乱。
dispatch_barrier_async
和dispatch_barrier_sync
的区别就是阻不阻当前线程
GCD信号量操作
信号量的API
dispatch_semaphore_t dispatch_semaphore_create(long value);
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
-
dispatch_semaphore_create
创建一个dispatch_semaphore_t
的信号量,并且创建的时候需要指定信号量的大小,dispatch_semaphore_wait
等待信号量,如果信号量的值为0,那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数
等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回
dispatch_semaphore_signal
发送信号量,该函数会对信号量的值进行+1操作。
并发队列,异步执行实现同步操作
首先建立一个并发队列,异步执行的操作
dispatch_queue_t queue = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"++1++%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"++2++%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"++3++%@",[NSThread currentThread]);
});
运行结果
++3++<NSThread: 0x600003b7c840>{number = 4, name = (null)}
++2++<NSThread: 0x600003b6d640>{number = 5, name = (null)}
++1++<NSThread: 0x600003b77a80>{number = 3, name = (null)}
从上述结果来看,队列中的任务并不是同步执行的,那么我们现在就可以使用信号量在进行同步操作,代码如下:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create("struggle3g", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"++1++%@",[NSThread currentThread]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
NSLog(@"++2++%@",[NSThread currentThread]);
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
NSLog(@"++3++%@",[NSThread currentThread]);
});
打印结果如下:
++1++<NSThread: 0x6000029e73c0>{number = 3, name = (null)}
++2++<NSThread: 0x6000029ef280>{number = 4, name = (null)}
++3++<NSThread: 0x6000029ef280>{number = 4, name = (null)}
让多个异步任务按照同步顺序执行,直接在同步队列异步操作不是更好吗?为什么要使用复杂的信号量,这是因为,同步队列中当中只是在一个线程当中运行,而信号量可以充分利用多线程,它会开启子线程。所以同步队列异步操作丧失了并发执行的可能性。虽然可以完成任务,但是却没有充分发挥CPU多线程的优势