Este capítulo presenta principalmente el GCD más utilizado en subprocesos múltiples.
- Introducción a GCD
- Cuatro combinaciones de funciones y colas (síncronas, asíncronas, seriales, paralelas)
- Consumo de energía de programación de rendimiento
- preguntas de entrevista
- intercambio de recursos de subprocesos
- función de barrera barrera
- Grupo de programación
- GCD singleton
- semáforo
1 Introducción a GCD
GCD, el nombre completo de Grand Central Dispatch (Central Dispatch Center), está desarrollado en lenguaje C puro y proporciona muchas funciones poderosas.
- Ventaja:
- GCD es la solución de Apple para computación paralela multinúcleo;
- GCD utilizará automáticamente más núcleos de CPU;
- GCD gestiona automáticamente el ciclo de vida de los hilos (crear hilos, programar tareas, destruir hilos).
- Los programadores solo necesitan decirle a GCD qué tareas desean realizar y no necesitan escribir ningún código relacionado con la gestión de subprocesos (independientemente de la programación y la destrucción)
- Núcleo: agregue una tarea a la cola y especifique una función para ejecutar la tarea
Aquí hay tres contenidos: tareas, colas y funciones que ejecutan tareas. Analizamos uno a uno
// 任务(block)
dispatch_block_t block = ^{
NSLog(@"hello GCD");
};
// 队列(此处串行队列)
dispatch_queue_t queue = dispatch_queue_create("syncTest", DISPATCH_QUEUE_SERIAL);
// 执行任务的函数(此处异步函数)
dispatch_async(queue, block);
复制代码
1.1 Tareas
La tarea de GCD es utilizar funciones encapsuladas en bloque sin parámetros de entrada y retorno. Una vez que se crea la tarea, la función que espera para ejecutarla la coloca en la cola.
1.2 Cola
Las colas GCD incluyen colas en serie y colas en paralelo. Cola en serie: solo se permite ejecutar una tarea a la vez. Cola paralela: permite ejecutar múltiples tareas al mismo tiempo.
1.3 Funciones que realizan tareas
Hay dos tipos de funciones que realizan tareas: funciones síncronas y funciones asíncronas:
- función de sincronización dispatch_sync:
- Debe esperar a que se complete la declaración actual antes de ejecutar la siguiente declaración
- El hilo no se abrirá y la tarea de bloque se ejecutará en el hilo actual
- función asíncrona dispatch_async:
- Ejecute la siguiente declaración sin esperar a que se complete la declaración actual
- Iniciará el hilo para ejecutar la tarea de bloque.
2 Cuatro combinaciones de funciones y colas (síncronas, asíncronas, en serie, paralelas)
-
Cola principal dispatch_get_main_queue:
- Una cola en serie dedicada a la programación de tareas en el subproceso principal
- el hilo no se iniciará
- Si el subproceso principal actual está ejecutando una tarea, debe esperar a que se complete la tarea actual antes de continuar con la programación de otras tareas.
-
Cola concurrente global dispatch_get_global_queue:
- Para comodidad de los programadores, Apple proporciona una cola global (cola concurrente, un atajo para satisfacer las necesidades de subprocesos múltiples).
- Al usar el desarrollo de subprocesos múltiples, si no hay un requisito especial para la cola, puede usar directamente la cola global para ejecutar tareas asincrónicas.
Q & A:
Q: 队列有几种?
// 串行队列
dispatch_queue_t serial = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
// 并行队列
dispatch_queue_t concurrent = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
// 主队列(串行队列)
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// 全局队列 (并行队列)
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
NSLog(@"\n%@ \n%@ \n%@ \n%@", serial, concurrent, mainQueue, globalQueue);
复制代码
A:只有串行队列和并行队列两种。
2.1 同步 + 串行 死锁
- (void)mainSyncTest{
NSLog(@"0 %@", [NSThread currentThread]);
// 等
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
}
复制代码
分析:
- 主队列(main)是串行队列,函数是sync同步函数。属于同步函数&串行队列的情况
- 在打印0之后,dispatch_sync同步函数将block排队插入mainSyncTest函数最后,等待mainSyncTest函数执行完后再执行。
- 但是block没有执行,dispatch_sync函数就等于没有完成。程序无法往下执行。
- 所以造成了dispatch_sync等mainSyncTest执行完后执行block,而mainSyncTest却说dispatch_sync没有执行完.
Q1 :如果上述代码,将同步 + 主队列执行改为同步 + 自定义串行队列,是否会堵塞?
dispatch_queue_t serial = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
NSLog(@"0 %@", [NSThread currentThread]);
dispatch_sync(serial, ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
复制代码
总结 虽然将主队列执行改为自定义串行队列,解决了堵塞问题,但是否和syncTest代码本身运行的队列和线程有关呢? [之前堵塞代码] 函数代码和block代码都在主队列+主线程 [新代码]函数代码在自定义队列+主线程
Q2: 将syncTest代码也放在这个自定义队列中执行,此时堵塞是否会出现?
dispatch_queue_t serial = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serial, ^{
[self mainSyncTest];
});
- (void)mainSyncTest{
NSLog(@"0 %@", [NSThread currentThread]);
// 等
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
}
复制代码
总结 我们注意到线程还是在main主线程。所以:同一队列同一线程进行sync同步操作,会阻塞,Crash 主队列是个特殊队列,APP启动时就与主线程完成了线程绑定。不会切换线程。
Q3 :自定义队列切其他自定义队列,是否也可以阻止阻塞呢? Q4 :是否和主线程有关?如果当前函数在子线程执行,任务回归主线程操作,是否也可以阻止阻塞呢?
总结 我们注意到线程还是在main主线程。所以:同一队列同一线程进行sync同步操作,会阻塞,Crash
继续探索,按照上面说的,自定义队列切主队列,是否也可以阻止阻塞呢? 总结: 主队列是个特殊队列,APP启动时就与主线程完成了线程绑定,不会切换线程。
- 当前环境:主队列+主线程,执行sync同步 + main_queue主队列任务,会阻塞
- 当前环境:主队列+主线程,切换到自定义串行队列,不会开辟线程(block在main线程执行),不会阻塞
- 当前环境:自定义串行队列+主线程,切换到主队列,会阻塞
- 当前环境:自定义串行队列+主线程,切换到新自定义串行队列,不会开辟线程(block在main线程执行),不会阻塞
- 当前环境:自定义串行队列+子线程,切换到主队列,主队列是绑定main线程的,所以会切换回main线程执行block任务,执行完后回到子线程,执行后续任务。不会阻塞
2.2 同步 + 并行
for (int i = 0; i<20; i++) {
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
复制代码
总结 不会阻塞线程,但是一次只通过一个。是耗时操作。
2.3 异步 + 串行
- (void)mainAsyncTest{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"1 %@", [NSThread currentThread]);
});
NSLog(@"2 %@", [NSThread currentThread]);
}
复制代码
可以发现,异步+串行时,异步函数内的Block(打印1)是在mainAsyncTest函数全部执行完后(打印了2),再在新线程中执行block,打印了1。
可以对比上面2.1 同步 + 串行 阻塞死锁的现象,两者的区别是: 同步 + 串行:
- dispatch_sync必须 等 mainSyncTest执行完,才将block任务插入尾部。
- dispatch_sync必须 等 block执行完,才算完成。
异步 + 串行:
- 1.dispatch_async 不用等 mainAsyncTest执行完,直接将block任务插入尾部。
- dispatch_async 不用等 block执行完,只要将block插入尾部,就算完成了。
2.4 异步 + 并行
for (int i = 0; i<20; i++) {
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
复制代码
总结 会开启多个线程,执行顺序不确定。
3. 性能调度耗能
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_SERIAL);
// dispatch_async(queue, ^{
// NSLog(@"异步执行");
// });
dispatch_sync(queue, ^{
NSLog(@"同步执行");
});
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
复制代码
对比无任何操作、创建线程、创建线程调用同步函数、创建线程调用异步函数四种情况的耗时:
- 无任何操作时,基本无耗时
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
复制代码
- 创建线程: 耗时0.00009秒
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread",
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
复制代码
- 创建线程且调用异步函数: 耗时0.00040秒
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"异步执行");
});
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
复制代码
- 创建线程且调用同步函数: 耗时0.000232秒
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("thread", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"同步执行");
});
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
复制代码
总结:
- 每次创建线程,都会有时间上的损耗
- 线程创建后,同步执行比异步执行更耗时
4. 面试题
面试题1:
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
});
NSLog(@"5");
复制代码
答案: 打印 1、5、2后,崩溃
面试题2 :
// 并行队列
dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1 %@",[NSThread currentThread]);
// 异步
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 -> 3 -> 4
- 与面试题一不同,这里是DISPATCH_QUEUE_CONCURRENT并行队列。
- 参考2.2 同步+并行分析,并发队列中的dispatch_sync同步函数不会阻塞线程,但是一次只通过一个任务。
面试题3:
// 并行队列
dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
// 耗时
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
复制代码
答案: 打印结果: 1 -> 5 -> 2 -> 4 -> 3
面试题4:
选出打印顺序可能出现的选项: A: 1230789 B: 1237890 C: 3120798 D: 2137890
// 并行队列
dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
复制代码
答案 是 A 和 C 分析:
- 是并发队列;
- 异步 & 并发是无序的,所以1和2的打印是无序的, 7、8、9的打印是无序的;
- 同步 & 并发是排队一个个任务执行,所以0一定在3后面打印,7、8、9一定在0后面打印。
- 满足0在3后打印,7、8、9在0后打印。只有选项 A 和 C。
5 线程资源共享
- 多读单写:
利用串行队列,异步函数支持多人买票,同步函数限制同一时刻仅出一张票。
@interface ViewController ()
@property (nonatomic, assign) NSInteger tickets; // 票数
@property (nonatomic, strong) dispatch_queue_t queue; // 队列
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 准备票数
_tickets = 20;
// 创建串行队列
_queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_SERIAL);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 第一个线程卖票
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTickes];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 第二个线程卖票
[self saleTickes];
});
}
- (void)saleTickes {
while (self.tickets > 0) {
// 模拟延时
[NSThread sleepForTimeInterval:1.0];
// 苹果不推荐程序员使用互斥锁,串行队列同步任务可以达到同样的效果!
// @synchronized
// 使用串行队列,同步任务卖票
dispatch_sync(_queue, ^{
// 检查票数
if (self.tickets > 0) {
self.tickets--;
NSLog(@"还剩 %zd %@", self.tickets, [NSThread currentThread]);
} else {
NSLog(@"没有票了");
}
});
}
}
@end
复制代码
6. 栅栏函数barrier
控制任务执行顺序,同步。
- dispatch_barrier_async:前面任务都执行完毕,才会到这里(不会堵塞线程)
- dispatch_barrier_sync:堵塞线程,等待前面任务都执行完毕,才放开堵塞。堵塞期间,后面的任务都被挂起等待。
重点:栅栏函数只能控制同一并发队列
- 栅栏函数只应用在并行队列&异步函数中,它的作用就是在监听多个信号(任务)是否都完成。
( 串行或同步内的信号(任务)本身就是按顺序执行,不需要使用到栅栏函数。)
坑点:栅栏函数为何不能使用dispatch_get_global_queue队列? 因为global队列中有很多系统任务也在执行。 我们需要dispatch_queue_create手动创建一个纯净的队列,放置自己需要执行的任务,再使用栅栏函数监听任务的执行结果。
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
__block CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
// 请求token
[self requestToken:^(id value) {
// 带token
[weakSelf requestDataWithToken:value handle:^(BOOL success) {
success ? NSLog(@"成功") : NSLog(@"失败");
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
}];
}];
}
/** 获取token请求 */
- (void)requestToken:(void(^)(id value))successBlock{
NSLog(@"开始请求token");
[NSThread sleepForTimeInterval:1];
if (successBlock) {
successBlock(@"b2a8f8523ab41f8b4b9b2a79ff47c3f1");
}
}
/** 请求所有数据 */
- (void)requestDataWithToken: (NSString *)token handle: (void(^)(BOOL success))successBlock {
dispatch_queue_t queue = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
[self requestHeadDataWithToken: token handle:^(id value) { NSLog(@"%@", value); }];
});
dispatch_async(queue, ^{
[self requestListDataWithToken:token handle:^(id value) { NSLog(@"%@", value); }];
});
dispatch_barrier_async(queue, ^{ successBlock(true); });
}
/** 头部数据的请求 */
- (void)requestHeadDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
if (token.length == 0) {
NSLog(@"没有token,因为安全性无法请求数据");
return;
}
[NSThread sleepForTimeInterval:2];
if (successBlock) {
successBlock(@"我是头,都听我的");
}
}
/** 列表数据的请求 */
- (void)requestListDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
if (token.length == 0) {
NSLog(@"没有token,因为安全性无法请求数据");
return;
}
[NSThread sleepForTimeInterval:1];
if (successBlock) {
successBlock(@"我是列表数据");
}
}
@end
复制代码
7 调度组 Group
与栅栏函数类似,也是控制任务的执行顺序。
- dispatch_group_create 创建组
- dispatch_group_async 进组任务 (自动管理进组和出组)
- dispatch_group_notify 进组任务执行完毕通知
- dispatch_group_wait 进组任务执行等待时间
- dispatch_group_enter 进组
- dispatch_group_leave 出组
进组和出组需要成对搭配使用
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
__block CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
// 1. 【手动入组和出组】
[self requestToken:^(id value) {
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrent = dispatch_queue_create("ypy", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_enter(group);
dispatch_async(concurrent, ^{
[weakSelf requestHeadDataWithToken:value handle:^(id value) {
NSLog(@"%@",value);
dispatch_group_leave(group);
}];
});
dispatch_group_enter(group);
dispatch_async(concurrent, ^{
[weakSelf requestListDataWithToken:value handle:^(id value) {
NSLog(@"%@",value);
dispatch_group_leave(group);
}];
});
dispatch_group_notify(group, concurrent, ^{
NSLog(@"成功了");
NSLog(@"%f", CFAbsoluteTimeGetCurrent() - time);
});
}];
}
/** 获取token请求 */
- (void)requestToken:(void(^)(id value))successBlock{
NSLog(@"开始请求token");
[NSThread sleepForTimeInterval:1];
if (successBlock) {
successBlock(@"b2a8f8523ab41f8b4b9b2a79ff47c3f1");
}
}
/** 头部数据的请求 */
- (void)requestHeadDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
if (token.length == 0) {
NSLog(@"没有token,因为安全性无法请求数据");
return;
}
[NSThread sleepForTimeInterval:2];
if (successBlock) {
successBlock(@"我是头,都听我的");
}
}
/** 列表数据的请求 */
- (void)requestListDataWithToken:(NSString *)token handle:(void(^)(id value))successBlock{
if (token.length == 0) {
NSLog(@"没有token,因为安全性无法请求数据");
return;
}
[NSThread sleepForTimeInterval:1];
if (successBlock) {
successBlock(@"我是列表数据");
}
}
@end
复制代码
- GCD单例
单例: 利用static在内存中仅一份的特性,保证了对象的唯一性。 重写allocWithZone的实现,让外界使用alloc创建时,永远返回的是static声明的对象。 以下是KCImageManger的核心代码:
// 保存在常量区
static id instance;
@implementation KCImageManger
/**
每次类初始化的时候进行调用
1、+load它不遵循那套继承规则。如果某个类本身没有实现+load方法,那么不管其它各级超类是否实现此方法,系统都不会调用。+load方法调用顺序是:SuperClass -->SubClass --> CategaryClass。
3、+initialize是在类或者它的子类接受第一条消息前被调用,但是在它的超类接收到initialize之后。也就是说+initialize是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是不会被调用的。
4、只有执行+initialize的那个线程可以操作类或类实例,其他线程都要阻塞等着+initialize执行完。
5、+initialize 本身类的调用都会执行父类和分类实现 initialize方法都会被调多次
*/
+ (void)initialize{
NSLog(@"父类");
if (instance == nil) {
instance = [[self alloc] init];
}
}
/**
配合上面 也能进行单利
*/
+ (instancetype)manager{
return instance;
}
/**
* 所有为类的对象分配空间的方法,最终都会调用到 allovWithZone 方法
* 下面这样的操作相当于锁死 该类的所有初始化方法
*/
+(instancetype)allocWithZone:(struct _NSZone *)zone{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [super allocWithZone:zone];
});
return instance;
}
/**
单利
*/
+(instancetype)shareManager{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
@end
复制代码
9 信号量 semaphore
控制GCD的最大并发数。(同一时刻可进行的信号(任务)最大个数。)
- dispatch_semaphore_create: 创建信号量
- dispatch_semaphore_wait: 信号量等待
- dispatch_semaphore_signal: 信号量释放
加入了信号量的等待dispatch_semaphore_wait后,一定需要配对加入信号量释放dispatch_semaphore_signal,不然会crash
// 创建全局队列(并行)
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 设置信号量
dispatch_semaphore_t sem = dispatch_semaphore_create(2); // 最多同时执行2个任务
//任务1
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
sleep(1);
NSLog(@"执行任务1");
sleep(1);
NSLog(@"任务1完成");
dispatch_semaphore_signal(sem);
});
//任务2
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
sleep(1);
NSLog(@"执行任务2");
sleep(1);
NSLog(@"任务2完成");
dispatch_semaphore_signal(sem);
});
//任务3
dispatch_async(queue, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
sleep(1);
NSLog(@"执行任务3");
sleep(1);
NSLog(@"任务3完成");
dispatch_semaphore_signal(sem);
});
复制代码