iOS学习笔记-多线程之GCD

148 阅读8分钟

本文为学习笔记,主要是参考文章内容的简化版,加深理解, 复习使用。

参考文章

iOS多线程:『GCD』详尽总结


一、使用GCD的好处

  • 可用于多核的并行运算

  • 会自动利用更多的CPU内核(比如双核、四核)

  • 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

  • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码


二、GCD任务和队列

GCD的两个核心概念【任务】【队列】

1. 任务

任务,就是执行操作的意思,换句话说就是你在线程中执行的那段代码;

执行任务有两种方式:【同步执行】【异步执行】。两者主要的区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

  • 同步执行(sync):

    1. 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里的任务完成后再继续执行;

    2. 只能在当前线程中执行任务,不具备开启新线程的能力。

  • 异步执行(async):

    1. 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务;

    2. 可以在新的线程中执行任务,具备开启新线程的能力(并不一定开启新线程)。

同步执行代码举例:

- (void)viewDidLoad {

    [super viewDidLoad];

    NSLog(@"准备开启线程, 当前线程:%@", [NSThread currentThread]);

    // 这里如果使用 "dispatch_get_main_queue()"主队列, 会造成死锁

    dispatch_sync(dispatch_get_global_queue(0, 0), ^{

       NSLog(@"开始执行任务, 当前线程:%@", [NSThread currentThread]);

       [NSThread sleepForTimeInterval:2];

       NSLog(@"结束任务, 当前线程:%@", [NSThread currentThread]);

    });

    NSLog(@"执行后续任务(代码), 当前线程:%@", [NSThread currentThread]);
}

输出结果:

准备开启线程, 当前线程:<_NSMainThread: 0x60000022c840>{number = 1, name = main}
开始执行任务, 当前线程:<_NSMainThread: 0x60000022c840>{number = 1, name = main}
结束任务, 当前线程:<_NSMainThread: 0x60000022c840>{number = 1, name = main}
执行后续任务(代码), 当前线程:<_NSMainThread: 0x60000022c840>{number = 1, name = main}

从上述代码和输出日志可以看出,将同步执行任务添加进队列后,该同步任务执行完成后,才会继续执行后续代码。

死锁说明:程序将viewDidLoad方法添加进主队列,所以在viewDidLoad方法执行完成后,主线程才会执行后续任务。但是在viewDidLoad方法中,又将一个同步执行任务添加进主队列,该同步任务必须等待主队列中的上一个任务执行结束,也就是viewDidLoad方法执行结束后,才能执行。这就造成了死锁。

异步执行代码举例:

- (void)viewDidLoad {

    [super viewDidLoad];

    NSLog(@"准备开启线程, 当前线程:%@", [NSThread currentThread]);

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"开始执行任务, 当前线程:%@", [NSThread currentThread]);

       [NSThread sleepForTimeInterval:2];

        NSLog(@"结束任务, 当前线程:%@", [NSThread currentThread]);

    });

    NSLog(@"执行后续任务(代码), 当前线程:%@", [NSThread currentThread]);
}

输出结果:

准备开启线程, 当前线程:<_NSMainThread: 0x600002f789c0>{number = 1, name = main}
执行后续任务(代码), 当前线程:<_NSMainThread: 0x600002f789c0>{number = 1, name = main}
开始执行任务, 当前线程:<NSThread: 0x600002f1c980>{number = 6, name = (null)}
结束任务, 当前线程:<NSThread: 0x600002f1c980>{number = 6, name = (null)}

从上述代码和输出日志可以看出,将异步执行任务添加进队列后,会继续执行后续代码,而该异步执行任务在2s后才执行完。

2. 队列

队列(Dispatch Queue),这里的队列指执行任务的等待队列,即用来等待任务的队列。队列是一种特殊的线性表,遵循FIFO(先进先出)的原则。

在GCD中有两种队列:【串行队列】【并发队列】。主要区别:执行顺序不同,以及开启线程数不同。

  • 串行队列(Serial Dispatch Queue)

    每次只有一个任务被执行。让任务一个接一个地执行。(只开启一个线程,一个任务执行完毕后再执行下一个任务)

  • 并发队列(Concurrent Dispatch Queue)

    可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务),并发功能只在异步(dispatch_async)方法下才有效

在这里明确一下 任务、队列、线程这三者的关系。最开始以为dispatch_get_main_queue() 方法拿到的就是主线程,dispatch_get_global_queue() 方法拿到的就是子线程。但其实这两个方法拿到的是队列

dispatch_async(dispatch_get_main_queue, ^{})方法做的是:将任务(block内的代码)加入到队列,然后系统从队列取出任务(先进先出),再使用相应的线程去执行任务。


三、GCD的使用步骤

  1. 创建一个队列

  2. 将任务追加到等待队列中,然后系统会根据任务类型去执行任务(同步执行或异步执行)

3.1 队列的创建/获取
// 串行队列的创建方法

dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);

// 并发队列的创建方法

dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT)
  • 创建方法需要传入两个参数:

    1. 第一个参数表示队列的唯一标识符,用于DEBUG,可为空。推荐使用应用程序ID这种逆序全程域名。

    2. 第二个参数用来标识是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。

  • 串行队列,GCD默认提供了主队列(Main Dispatch Queue)

    1. 所以放在主队列的任务,都会放到主线程执行

    2. 可使用 dispatch_get_main_queue() 方法获得主队列

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

3.2 任务和队列不同组合方式的区别

默认当前线程为主线程的情况下(暂不考虑队列中嵌套队列的复杂情况):

区别并发队列串行队列主队列
同步(sync)没有开启新线程,串行执行任务没有开启新线程,串行执行任务死锁,卡住不执行
异步(async)有开启新线程,并发执行任务有开启新线程(1条),串行执行任务没有开启新线程,串行执行任务

关于死锁,实际上在使用 串行队列 的时候,也可能会出现死锁问题。多见于同一个串行队列的嵌套使用。

如在 异步执行 + 串行队列 的任务中,又嵌套了 同步执行 + 当前队列,代码如下

dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue, ^{ // 异步执行 + 串行队列

    dispatch_sync(queue, ^{ // 同步执行 + 当前串行队列

    // 追加任务 1

    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作

    NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程

    });
});
3.3 对任务和队列开启线程能力的理解

假设现在有 5 个人要穿过一道门禁,这道门禁总共有 10 个入口,管理员可以决定同一时间打开几个入口,可以决定同一时间让一个人单独通过还是多个人一起通过。不过默认情况下,管理员只开启一个入口,且一个通道一次只能通过一个人。

  • 这个故事里,人好比是 任务,管理员好比是 系统,入口则代表 线程。

  • 5 个人表示有 5 个任务,10 个入口代表 10 条线程。

    • 串行队列 好比是 5 个人排成一支长队。
    • 并发队列 好比是 5 个人排成多支队伍,比如 2 队,或者 3 队。
    • 同步任务 好比是管理员只开启了一个入口(当前线程)。
    • 异步任务 好比是管理员同时开启了多个入口(当前线程 + 新开的线程)。
  • 『异步执行 + 并发队列』 可以理解为:现在管理员开启了多个入口(比如 3 个入口),5 个人排成了多支队伍(比如 3 支队伍),这样这 5 个人就可以 3 个人同时一起穿过门禁了。

  • 『同步执行 + 并发队列』 可以理解为:现在管理员只开启了 1 个入口,5 个人排成了多支队伍。虽然这 5 个人排成了多支队伍,但是只开了 1 个入口啊,这 5 个人虽然都想快点过去,但是 1 个入口一次只能过 1 个人,所以大家就只好一个接一个走过去了,表现的结果就是:顺次通过入口。

  • 换成 GCD 里的语言就是说:

    • 『异步执行 + 并发队列』就是:系统开启了多个线程(主线程+其他子线程),任务可以多个同时运行。
    • 『同步执行 + 并发队列』就是:系统只默认开启了一个主线程,没有开启子线程,虽然任务处于并发队列中,但也只能一个接一个执行了。

四、GCD的使用

以上的内容主要是概念方面的理解,具体代码操作详见参考文章iOS多线程:『GCD』详尽总结,模块4 至 模块6