iOS进阶 -- 多线程 GCD 队列与函数

前言

作为一名iOS开发者,我们都知道在iOS中常用的多线程管理方式有三种,NSThread、GCD和NSOperation。三者的对比如下:

  • NSThread提供了创建线程、调度线程、销毁线程等接口,但是接口比较复杂,需要程序员自己管理线程的生命周期,所以更多是用在调试时。
  • NSOperation是基于GCD的OC封装,与GCD很类似,NSOperation有队列和操作两个概念,两者配合实现多线程。
  • GCD,全称Grand Central Dispatch,是苹果提供的多线程管理方案,会自动利用更多的CPU内核,并且可以自动管理线程的生命周期,例如创建线程、调度线程及销毁线程,这些都不用程序员操心,程序员只需要告诉GCD想要执行什么任务,而不用写任何线程管理代码。

本篇就来初探下GCD的队列与函数。

一、队列与函数

在探索队列和函数之前,先看一个demo,代码如下:

Xnip2021-10-05_15-44-11.png

如图所示,为一个GCD的简单调用,在这个demo中,创建了一个 打印执行任务 的任务,同时创建了一个串型队列,并将任务添加到队列中,由一个异步函数执行。这一过程可以概括为将任务添加到队列,并且指定执行任务的函数

在GCD中,任务被封装成了一个个的block,即dispatch_block_t,我们可以将要执行的代码放在这样的block中。

1.1 队列

GCD还提供了一个函数 dispatch_queue_create,代码定义如下:

/*!

 * **@function** dispatch_queue_create
 *
 * **@abstract**
 * Creates a new dispatch queue to which blocks may be submitted.
 * 创建一个可以提交代码块的新队列

 * **@param** label
 * A string label to attach to the queue.
 * This parameter is optional and may be NULL.
 *
 * **@param** attr
 * A predefined attribute such as DISPATCH_QUEUE_SERIAL,
 * DISPATCH_QUEUE_CONCURRENT, or the result of a call to
 * a dispatch_queue_attr_make_with_* function.
 *

 * **@result**
 * The newly created dispatch queue.
 */

API_AVAILABLE(macos(10.6), ios(4.0))

DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT

DISPATCH_NOTHROW

dispatch_queue_t

dispatch_queue_create(**const** **char** * **_Nullable** label,

dispatch_queue_attr_t **_Nullable** attr);
复制代码

该函数包含两个参数,第一个参数是队列的唯一标识,用于区分队列,第二个参数表示队列的类型,即串行队列并发队列,分别由 DISPATCH_QUEUE_SERIALDISPATCH_QUEUE_CONCURRENT标识。

串行队列和并发队列的区别如下图:

串行队列和并发队列

  • 串行队列中任务只能串行调度,任务依次执行
  • 并发队列可以在同一时间段内,同时调度多个任务,具备并发能力

1.2 函数

然后将创建的队列和任务作为参数传递到执行函数中,GCD提供了两种函数 dispatch_asyncdispatch_sync,分别表示异步和同步函数。

同步函数和异步函数的区别如下图:

同步函数和异步函数的区别

如图所示,假定任务3是一个耗时任务,当采用同步函数时,执行到任务3,就会阻塞后续的任务执行,后续任务必须等到任务3执行完毕后才能得以执行,俗称“护犊子”;而如果采用异步函数,执行到任务3时,后续任务会继续执行,不会被阻塞。

以一个demo对比验证如下:

异步函数执行步骤

同步函数执行步骤

对比可以看出,采用异步函数时, task2中的sleep不会导致task3的阻塞,而采用同步函数时,task3必须等待task2执行完毕,由此可以验证上面的结论。并且从结果中可以发现,异步函数拥有开启新的线程的能力,而同步函数不会开启新线程。

二、函数与队列的组合

在上一小节中,我们知道GCD中函数有两种 同步和异步,队列分为两种 串行和并发,加上两种特殊的队列 主队列全局并发队列,所以可以有如下几种组合:

  • 同步函数 + 主队列
  • 同步函数 + 全局并发队列
  • 同步函数 + 普通串行队列
  • 同步函数 + 普通并发队列
  • 异步函数 + 主队列
  • 异步函数 + 全局并发队列
  • 异步函数 + 普通串行队列
  • 异步函数 + 普通并发队列

其实主队列是一种特殊的串行队列,全局并发队列是一种特殊的并发队列,不过在某些情况下与普通队列有所不同,我们分别通过demo来看下这几种组合会有什么效果。

2.1 同步函数 + 主队列

同步函数 + 主队列的代码如下:

- (void)syncMain {

    dispatch_queue_t mainQueue = dispatch_get_main_queue();

    NSLog(@"1 ---- %@",[NSThread currentThread]);

    dispatch_sync(mainQueue, ^{

        NSLog(@"2 ---- %@",[NSThread currentThread]);

    });

    NSLog(@"3 ---- %@",[NSThread currentThread]);
}
复制代码

代码执行结果如下:

Xnip2021-10-05_19-21-07.png 可以看到同步函数 + 主队列的执行结果是发生了死锁,_dispatch_sync_f_slow () 是发生死锁时GCD调用的函数。发生死锁的原因如下:

Xnip2021-10-05_19-46-30.png

2.2 异步函数 + 主队列

异步函数 + 主队列的代码如下:

- (void)asyncMain {
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    NSLog(@"1 ---- %@",[NSThread currentThread]);
    dispatch_async(mainQueue, ^{
        NSLog(@"2 ---- %@",[NSThread currentThread]);
    });
    NSLog(@"3 ---- %@",[NSThread currentThread]);
}
复制代码

代码执行结果如下:

Xnip2021-10-05_19-51-44.png 从执行结果可以发现,使用异步函数没有阻塞后面的任务,因此也不会发生死锁。并且可以发现,在主队列下使用异步函数也不会开启新的线程。

2.3 同步函数 + 全局并发队列

同步函数 + 全局并发队列的代码如下:

- (void)syncGlobal {
    dispatch_queue_t global = dispatch_get_global_queue(0, 0);
    NSLog(@"1 ---- %@",[NSThread currentThread]);
    dispatch_sync(global, ^{
        NSLog(@"2 ---- %@",[NSThread currentThread]);
        dispatch_sync(global, ^{
            NSLog(@"3 ---- %@",[NSThread currentThread]);
        });
        NSLog(@"4 ---- %@",[NSThread currentThread]);
    });
    NSLog(@"5 ---- %@",[NSThread currentThread]);
}
复制代码

这次代码稍微复杂一些,内部再嵌套了一个sync函数,其执行结果如下:

Xnip2021-10-05_19-59-33.png 通过结果可以发现,在全局并发队列下,使用sync也不会阻塞,更不会死锁。并且可以发现同步函数不会开启新的线程,因此虽然是在全局并发队列中,但是依然是在主线程

2.4 异步函数 + 全局并发队列

同步函数 + 全局并发队列的代码如下:

- (void)syncGlobal {
    dispatch_queue_t global = dispatch_get_global_queue(0, 0);
    NSLog(@"1 ---- %@",[NSThread currentThread]);
    dispatch_async(global, ^{
        NSLog(@"2 ---- %@",[NSThread currentThread]);
        dispatch_async(global, ^{
            NSLog(@"3 ---- %@",[NSThread currentThread]);
        });
        NSLog(@"4 ---- %@",[NSThread currentThread]);
    });
    NSLog(@"5 ---- %@",[NSThread currentThread]);
}
复制代码

代码执行结果如下:

Xnip2021-10-05_20-06-36.png 可以发现代码是异步执行,不会阻塞,也不会死锁,并且async函数会开启新的线程6和7。

2.5 同步函数 + 普通串行队列

同步函数 + 普通串行队列的代码,我们分两部分看,先看第一部分如下

- (void)syncSerial {
    dispatch_queue_t serial = dispatch_queue_create("BPTest.sync.Serial", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1 ---- %@",[NSThread currentThread]);
    dispatch_sync(serial, ^{
        NSLog(@"2 ---- %@",[NSThread currentThread]);
//        dispatch_sync(serial, ^{
//            NSLog(@"3 ---- %@",[NSThread currentThread]);
//        });
//        NSLog(@"4 ---- %@",[NSThread currentThread]);
    });
    NSLog(@"3 ---- %@",[NSThread currentThread]);
}
复制代码

执行结果如下:

Xnip2021-10-05_20-19-05.png

可以发现此处并未发生死锁,对比同步函数 + 主队列,同样都是串行队列,为何主队列会死锁闪退,而普通的串行队列可以正常运行呢?其原因如下图:

Xnip2021-10-05_20-29-46.png

同步函数 + 主队列中,因为只有一个主队列,所以会发生死锁,而在同步函数 + 串行队列中,因为有除了有一个串行队列外,还有个默认的主队列,我们的次任务是添加到串行队列中的,因此不会死锁闪退。

那么同步函数 + 串行队列一定是安全的吗?我们接着看下面的代码:

Xnip2021-10-05_20-42-49.png

可以发现,同步函数 + 串行队列一样会发生闪退,那么我们分析下这次死锁闪退的原因,如下图所示:

Xnip2021-10-05_20-54-17.png

其实原因与主队列闪退是一致的,本次所添加的任务都是添加到我们创建的串行队列中,所以会发生和主队列一样死锁闪退。

2.6 异步函数 + 普通串行队列

异步函数 + 普通串行队列的代码如下:

- (void)asyncSerial {
    dispatch_queue_t serial = dispatch_queue_create("BPTest.async.Serial", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1 ---- %@",[NSThread currentThread]);
    dispatch_async(serial, ^{
        NSLog(@"2 ---- %@",[NSThread currentThread]);
        dispatch_async(serial, ^{
            NSLog(@"3 ---- %@",[NSThread currentThread]);
        });
        NSLog(@"4 ---- %@",[NSThread currentThread]);
    });
    NSLog(@"5 ---- %@",[NSThread currentThread]);
}
复制代码

代码执行结果如下:

Xnip2021-10-05_21-00-07.png

虽然还是添加在串行队列中,但是因为使用的是异步函数,不会发生阻塞,所以也不会产生死锁。

2.7 同步函数 + 普通并发队列

同步函数 + 普通并发队列的代码如下:

- (void)syncConcurrent {
    dispatch_queue_t concurrent = dispatch_queue_create("BPTest.sync.concurrent", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1 ---- %@",[NSThread currentThread]);
    dispatch_sync(concurrent, ^{
        NSLog(@"2 ---- %@",[NSThread currentThread]);
        dispatch_sync(concurrent, ^{
            NSLog(@"3 ---- %@",[NSThread currentThread]);
        });
        NSLog(@"4 ---- %@",[NSThread currentThread]);
    });
    NSLog(@"5 ---- %@",[NSThread currentThread]);
}
复制代码

执行结果如下:

Xnip2021-10-05_21-02-52.png

同步函数 + 全局并发队列的情况一致,也不会发生死锁闪退,这里分析下为何使用并发队列没有闪退,而用串行队列闪退了,分析如图:

Xnip2021-10-05_21-09-58.png

2.8 异步函数 + 普通并发队列

代码及执行结果如下:

Xnip2021-10-05_21-11-27.png 这里既不会阻塞,也不会死锁。

总结

本篇主要介绍了GCD的队列和函数,可以得到以下几个结论:

  • 1、函数分为同步函数和异步函数,函数控制是否有开辟线程的能力,同步函数不具有开启新线程的能力,异步函数具有开辟新线程的能力,但是会根据实际情况确定是否开辟新线程
  • 2、队列主要分为串行队列和并发队列,队列决定了线程的调度能力,串行队列只能调度一个线程,因此任务只能顺序执行,并发队列则具有并发调度的能力。
  • 队列和函数的组合有以下几个结果:
同步 异步
主队列 死锁闪退 正常,但不会开辟新线程(主队列中只有主线程)
全局并发队列 正常,同步不开辟新线程 正常,开辟新线程
普通串行队列 部分情况下会死锁闪退 正常,会开辟新线程
普通并发列 正常,同步函数不开启新线程 正常,会开辟新线程

以上即为对于GCD队列与函数的初步探索,后续会继续探索GCD源码,从中进一步学习。本篇探索到此为止,如果有不准确的地方,欢迎大家指正。

猜你喜欢

转载自juejin.im/post/7015571874897723406