【Objective -- C】—— GCD(1)(Grand Central Dispatch)

GCD

1. 什么是GCD

GCD的全称是Grand Central Dispatch,是异步执行任务的技术之一。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。为了开发者的方便,我们只需要定义想执行的任务并追加到适当的dispatch Queue中GCD就能自动生成必要的线程并计划执行任务,提升了效率。

为什么使用GCD

  • GCD 可用于多核的并行运算;
  • GCD 会自动利用更多的 CPU 内核(比如双核、四核);
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
    接下来详细了解并学习GCD,书上简单的代码介绍了他的强大之处:
    在这里插入图片描述
    在上述GCD中的例子是经过了长时间的处理结束的时候,主线程使用该处理结果的源代码。仅用了一行代码让处理在后台线程中进行。
dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"主线程");
        // 主线程操作
    });

2.多线程编程

线程

一段从上到下顺序执行的代码是先通过编译器转换为CPU命令行二进制代码。一段从上到下顺序执行的代码是先通过编译器转换为CPU命令行二进制代码。
在这里插入图片描述
之后编译器汇集CPU命令列和数据将其安装到手机或电脑上。对于OS X和iOS的处理顺序是从CPU应用程序指定的地址开始一个一个的执行CPU命令列循环下去。

对于OC机制来说,由于if 和for循环的存在执行命令列的地址会是进行位置迁移,但是CPU一次只能执行一个命令不能执行某处分开的并列命令,因此CPU执行的命令列好比一条无分差的道路,执行不会出现分歧。
在这里插入图片描述
一次CPU执行的CPU命令列为一条无分叉路经-线程,多线程就是下图的多条无分叉路,此时多线程里一个CPU执行多条不同路径上的不同命令。
在这里插入图片描述

多线程的理解

OS X的核心XNU内核在发生操作系统时间的时候会切换执行路经。例如CPU的寄存器信息保存在各自路经专用的内存块里,从切换目标路经专用的内存块里复原CPU寄存器的信息,继续执行切换路经的CPU命令列叫上下文下切换
多线程编程就是多个CPU核反复进行某个线程和其他线程的上下文切换,叫多线程编程技术。
多线程编程存在:死锁,过于消耗内存,数据竞争等,但多线程编程可以保证程序的响应性能,利大于弊。
在这里插入图片描述

多线程的优点

多线程编程可以保证把一些费时间的处理放在主线程之外,这是提高系统性能的关键,也就是刚开始说的长时间的处理不在主线程而在其他线程的原因。

3.任务和队列

任务

就是执行操作的意思,在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:『同步执行』 和 『异步执行』。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

同步执行和异步执行
  • 同步执行sync
    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
    • 只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步执行async
    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
    • 可以在新的线程中执行任务,具备开启新线程的能力
    • 异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。

队列

队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。
在这里插入图片描述

串行队列和并发队列
  • 串行队列
    • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务 )
      在这里插入图片描述
  • 并发队列
    • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
    • 并发队列只有在异步执行dispatch_async方法下有效。
      在这里插入图片描述

GCD的API

GCD大大简化偏于复杂的多线程编程的源代码,封存在一个个API里。

1.Dispatch Queue

GCD:开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中。

 dispatch_async(queue, ^{
        // 想执行的任务
    });

使用block语法“定义想执行的任务”,通过dispatch_async函数追加赋值在变量queue的Dispatch Queue中,即可使指定的block在另一个线程执行。

Dispatch Queue是执行处理的等待队列,和上面讲的一样的,FIFO先进先出的原则执行。
在这里插入图片描述

Dispatch Queue的分类

Dispatch Queue在执行处理的时候分为 等待现在执行中处理的 Serial Dispatch Queue,不等待现在之中处理的 Concurrent Dispatch Queue

Dispatch Queue说明
Serial Dispatch Queue等待现在执行处理结束
Concurrent Dispatch Queue不等待现在执行处理结束

在这里插入图片描述
比较不同的Queue,追加多个处理的时候。

	dispatch_async(queue, blk0);
    dispatch_async(queue, blk1);
    dispatch_async(queue, blk2);
    dispatch_async(queue, blk3);
    dispatch_async(queue, blk4);
    dispatch_async(queue, blk5);

当变量为Serial Dispatch Queue,变量一定是遵循等待现在执行结束的原则,也就是按照blk0-blk5执行顺序。
当变量为Concurrent Dispatch Queue,变量遵循无需等待现在执行结束,执行bk0之后紧接着执行blk1,blk0是否结束不影响blk1的执行,这就是并行执行。

并行执行也就是多个线程同时进行多个处理,一个图理解一下并发执行,单个线程仍然是遵循一个线程一个任务。
在这里插入图片描述

2. dispatch_queue_create 方法

创建串行并行队列
在上一条未说明如何获取dispatch_queue的queue,这里是dispatch_queue_create 方法可以生成queue。
dispatch_queue_create 方法,该方法需要传入两个参数

dispatch_queue_create(const char *_Nullable DISPATCH_UNSAFE_INDEXABLE label,
		dispatch_queue_attr_t _Nullable attr);

  • 第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。队列的名称推荐使用应用程序 ID 这种逆序全程域名。
  • 第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。
// 串行队列的创建方法
    dispatch_queue_t serialQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
    // 并发队列的创建方法
    dispatch_queue_t concurrentQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);

2.1 serialQueue的并行

在这里插入图片描述
dispatch_queue_t 方法可以生成任意多的 Dispatch_queue,当生成多个 serialQueue的时候各个 serialQueu将并行执行。这么做会大大降低响应性能。
在这里插入图片描述
还会产生数据竞争
在这里插入图片描述
所以还是建议Serial Queue使用一个线程。如果想不发生竞争问题还是使用并行队列 Concureent Queue系统XNU内核会合理的管理线程。

在Queue里执行指定的block:

 dispatch_queue_t concurrentQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(concurrentQueue, ^{
        NSLog(@"blk on ConCurrentQueue");
    });

2.2dispatch_release方法

书上的意思是尽管在ARC下我们还需要手动释放DispatchQueue dispatch_release。

3.Main Dispatch Queue & Global Dispatch Queue

对于串行队列,GCD 默认提供了:主队列(Main Dispatch Queue)追加到Main Dispatch Queue的处理在主线程的RunLoop中执行。
在这里插入图片描述
所有放在主队列中的任务,都会放到主线程中执行。
可使用 dispatch_get_main_queue() 方法获得主队列。

// 主队列的获取方法
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

主队列其实并不特殊。 主队列的实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码,有都会放到主线程中去执行,所以才造成了主队列特殊的现象

对于并行队列 GCD 默认提供了全局并发队列(Global Dispatch Queue)
使用 dispatch_get_global_queue 方法

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

Global Dispatch Queue 有四个执行优先级。XNU内核管理的线程,将各自使用的Global Dispatch Queue的执行优先级作为线程的优先级使用。在Global Dispatch Queue里追加处理的时候应选择与处理内容对应的执行优先级的Global Dispatch Queue。
在这里插入图片描述
不同优先级Queue获取方法:

// global 高优先级
    dispatch_queue_t globalDispatchQueueHigh = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    
    // 默认优先级
    dispatch_queue_t globalDispatchQueueDefault = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 低优先级
    dispatch_queue_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    // 后台优先级
    dispatch_queue_t globalDispatchQueueBackGround = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

4.dispatch_set_target_queue函数

变更优先级

dispatch_queue_create 方法生成的Queue不论是串行还是并行队列,都是和globalQueue的默认优先级相同执行优先级的线程,对于变更优先级我们需要使用dispatch_set_target_queue函数

  • 第一个参数是需要变更优先级的参数,第二个参数指定了与那个优先级相同的队列。
 dispatch_set_target_queue(serialQueue, globalDispatchQueueBackGround);

添加执行层次

除此之外,dispatch_set_target_queue函数还可以做成Dispatch Queue的执行阶层。
在这里插入图片描述
也就是说在必须将不可并行执行的处理追加到多个串行队列的时候可以使用,dispatch_set_target_queue函数将目标变为某一个串行队列防止处理并发执行,我理解为防止消耗太多内存,毕竟串行队列还是一个线程最好。

5.dispatch_after GCD延迟执行方法

dispatch_after函数是作为我们设定在多少秒之后执行处理的函数。

dispatch_after(dispatch_time_t when, dispatch_queue_t  _Nonnull queue, ^(void)block);
  • dispatch_after第一个参数是指定时间用的dispatch_time_t类型的值。

dispatch_time_t 值使用如下函数:

dispatch_time_t
dispatch_time(dispatch_time_t when, int64_t delta);

dispatch_after提供了更高的延时精度。它使用的是系统的底层时间管理机制,可以实现相对准确的延时。而NSTimer的精度受到运行循环(run loop)的影响,可能会受到其他事件的干扰,导致延时不够准确。

6. Dispatch Group—GCD队列组

当出现多个处理全部执行结束的时候回到主队列执行结束处理,这种情况经常出现,这个时候GCD队列组的出现能帮助我们实现。

  • 调用队列组的 dispatch_group_async 先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enter、dispatch_group_leave 组合来实现 dispatch_group_async。
  • 调用队列组的 dispatch_group_notify 回到指定线程执行任务。或者使用 dispatch_group_wait 回到当前线程继续向下执行(会阻塞当前线程。

使用队列组的原因
无论向如何类型的Queue追加处理,Dispatch Group都可以监视这些处理直到执行结束,一旦执行结束,就可以将结束处理追加到DispatchQueue中。

dispatch_group_notify
监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。
参数:队列组,回到的线程,执行的block

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk0");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk3");
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"task all Done");
    });

dispatch_group_wait
暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。

  • 参数:队列组,第二参数指定为等待的时间(超时),DISPATCH_TIME_FOREVER意味永久等待,也就是队列组尚未结束就不会取消等待。
- (void)groupWait {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk0");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk2");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"blk3");
    });
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"group end");
}

当所有任务执行完成之后,才执行 dispatch_group_wait 之后的操作。但是,使用dispatch_group_wait 会阻塞当前线程。
dispatch_group_enter、dispatch_group_leave

  • dispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1。
  • dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。
  • 当 group 中未执行完毕任务数为0的时候,才会使 dispatch_group_wait 解除阻塞,以及执行追加到 dispatch_group_notify 中的任务。

7. GCD 栅栏方法:dispatch_barrier_async

在访问数据库或者文件的时候serial Queue可以避免数据竞争,其实这个场景就是我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。
我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务 GCD栅栏方法 dispatch_barrier_async函数。多用于异步操作。

  • 该函数同queue creat生成的Concurrent Dispatch Queue一起使用
/**
 * 栅栏方法 dispatch_barrier_async
 */
- (void)barrier {
    dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 2
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"2---%@",[NSThread currentThread]);      // 打印当前线程
    });
    
    dispatch_barrier_async(queue, ^{
        // 追加任务 barrier
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
    });
    
    dispatch_async(queue, ^{
        // 追加任务 3
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"3---%@",[NSThread currentThread]);      // 打印当前线程
    });
    dispatch_async(queue, ^{
        // 追加任务 4
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"4---%@",[NSThread currentThread]);      // 打印当前线程
    });
}

dispatch_barrier_async 方法原理

dispatch_barrier_async 方法会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在 dispatch_barrier_async 方法追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。
在这里插入图片描述

8. dispatch_sync 同步线程

GCD 提供了同步执行任务的创建方法 dispatch_sync 和异步执行任务创建方法 dispatch_async。

// 同步执行任务创建方法
dispatch_sync(queue, ^{
    // 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
    // 这里放异步执行任务代码
});

dispatch_sync & dispatch_async

dispatch_async意味着非同步,就是将制定的block非同步的追加到queue中, dispatch_async不会进行任何等待
在这里插入图片描述
dispatch_sync意味着同步,就是将制定的block同步的追加到queue中, dispatch_sync会在追加结束之前一直等待
在这里插入图片描述
死锁

  • 一旦使用sync函数,那么在指定处理结束之前函数不会返回,但它容易引起死锁。
- (void)syncLockForever {
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue,^ {
        NSLog(@"hello");
    });
}

该代码在主线程执行打印blk,并等待执行结束,而其实在主线程中还是在执行这一段代码,所以无法执行结束,也就是无法追加到主线程就会造成死锁,从而造成了无限的套娃行为。

9. dispatch_apply-GCD 快速迭代

dispatch_apply函数使dispatch_sync函数和Dispatch Group关联的API,它可以按照指定的次数将指定的blk追加到指定的Queue中,也可以理解成for循环的作用,并等待全部执行结束

  • dispatch_apply函数一般多用于并行队列,若是在串行队列使用其实for循环原理相似。
  • 参数:第一个是重复次数,第二个参数是追加对象的QUeue,第三个参数就是对追加参数的处理,整体结构和for循环很类似。
/**
 * 快速迭代方法 dispatch_apply
 */
- (void)apply {
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    NSLog(@"apply---begin");
    dispatch_apply(6, queue, ^(size_t index) {
        NSLog(@"%zd---%@",index, [NSThread currentThread]);
    });
    NSLog(@"apply---end");
}

dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作与dispatch_sync函数相同,也像是队列组中的 dispatch_group_wait方法。
由此推断dispatch_async里是非同步的执行了dispatch_apply函数。

10. dispatch_suspend & dispatch_resume

这两个函数可以随时挂起某个队列,等需要执行的时候恢复即可。

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_suspend(queue);
    dispatch_resume(queue);

11.dispatch_semaphore-GCD 信号量

GCD 中的信号量是指 Dispatch Semaphore:持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数小于0 时等待,不可通过。计数为0或大于0时,计数减1且不等待,可通过。
使用时机

信号量的使用时机是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量
Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务。
  • 保证线程安全,为线程加锁。

dispatch_semaphore方法

  • dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量
  • dispatch_semaphore_signal:发送一个信号,让信号总量加 1
  • dispatch_semaphore_wait:可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。

Dispatch Semaphore 使线程同步

/**
 * semaphore 线程同步
 */
- (void)semaphoreSync {
    
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    __block int number = 0;
    dispatch_async(queue, ^{
        // 追加任务 1
        [NSThread sleepForTimeInterval:2];              // 模拟耗时操作
        NSLog(@"1---%@",[NSThread currentThread]);      // 打印当前线程
        
        number = 100;
        
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"semaphore---end,number = %d",number);
}

理解:首先async是异步执行,我们要把异步变同步是需要添加一个等待的,也就是编译器

  • 1.在执行dispatch_async的时候 dispatch_semaphore_wait同时执行 此时信号量是-1
  • 2.当前线程阻塞,接着 dispatch_semaphore_signal(semaphore)信号量为0,线程继续进行,-
  • 3.number= 100.最后打印 semaphore—end,number = 100。

12.dispatch_once

我们在创建单例或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 方法。使用 dispatch_once 方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。

如此创建的单例模式只会被初始化一次。

先声明静态全局变量,只能在当前文件访问 ,单例对象的条件
#import "Person.h"
static id instance;
static id SingleEg;
// 懒汉
@implementation Person
+(id) instance {
    static dispatch_once_t o;
    dispatch_once(&o, ^{
        if (instance == nil) {
            instance = [[super alloc] init];
        }
    });
    return  instance;
}
// 饿汉
+ (id)_singleEg {
    static dispatch_once_t o;
    dispatch_once(&o, ^{
            SingleEg = [[super alloc] init];
    });
    return  SingleEg;
}

并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。

  • 10
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值