阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)

多线程

撸面试题中,文中内容基本上都是搬运自大佬博客及自我理解,可能有点乱,不喜勿喷!!!

主要以GCD为主

1、iOS开发中有多少类型的线程?分别对比

  1. Pthreads : 跨系统 c 语言多线程框架,不推荐。
  2. NSThread : ## 面向对象,需手动管理生命周期。
  3. GCD : Grand Central Dispatch,主打任务与队列,告诉他要做什么即可。
  4. NSOperation & NSOperationQueue : GCD 的封装,面向对象

2、GCD有哪些队列,默认提供哪些队列

  1. 主队列

    dispatch_get_main_queue()

  2. 全局并发队列

    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

    队列优先级从高到底为:

    DISPATCH_QUEUE_PRIORITY_HIGH

    DISPATCH_QUEUE_PRIORITY_DEFAULT

    DISPATCH_QUEUE_PRIORITY_LOW

    DISPATCH_QUEUE_PRIORITY_BACKGROUND

  3. 自定义队列(串行 Serial 与并行 Concurrent)

    dispatch_queue_create("这里是队列名字", DISPATCH_QUEUE_SERIAL)

    串行 DISPATCH_QUEUE_SERIAL

    并行 DISPATCH_QUEUE_CONCURRENT

3、GCD有哪些方法api

  • 队列

dispatch_get_main_queue()

dispatch_get_global_queue()

dispatch_queue_create()

  • 执行

dispatch_async()

dispatch_sync()

dispatch_after()

dispatch_once()

dispatch_apply()

dispatch_barrier_async()

dispatch_barrier_sync()

  • 调度组

dispatch_group_create()

dispatch_group_async()

dispatch_group_enter()

dispatch_group_leave()

dispatch_group_notify()

dispatch_group_wait()

  • 信号量=

dispatch_semaphore_create()

dispatch_semaphore_wait()

dispatch_semaphore_signal()

  • 调度资源

dispatch_source_create()

dispatch_source_set_timer()

dispatch_source_set_event_handler()

dispatch_resume()

dispatch_suspend()

dispatch_source_cancel()

dispatch_source_testcancel()

dispatch_source_set_cancel_handler()

4、GCD主线程 & 主队列的关系

提交到主队列的任务在主线程执行。

5、如何实现同步,有多少方式就说多少

dispatch_sync()

dispatch_barrier_sync()

dispatch_group_create() + dispatch_group_wait()

dispatch_apple()

dispatch_semaphore_create() + dispatch_semaphore_wait()

[NSOpertaion start]

NSOperationQueue.maxConcurrentOperationCount = 1

锁 pthread_mutex

NSLock

NSRecursiveLock

NSConditionLock & NSCondition

6、dispatch_once实现原理

iOS源码解析: dispatch_once是如何实现的?

  1. 读取 token 值 dispatch_once_t.dgo_once

  2. 若 Block 已完成,return;

  3. 若 Block 没有完成,尝试原子性修改 dispatch_once_t.dgo_once 值为 DLOCK_ONCE_UNLOCKED

    3.1 修改成功,执行 Block,原子性修改 dispatch_once_t.dgo_onceDLOCK_ONCE_DONE; 然后唤醒等待的线程

    3.2 若失败,则进入循环等待

7、什么情况下会死锁

A 等 B,B 等 A。

8、有哪些类型的线程锁,分别介绍下作用和使用场景

种类 备注
OSSpinLock 自旋锁 不安全,iOS 10 已启用
os_unfair_lock 互斥锁 替代 OSSpinLock
pthread_mutex 互斥锁 PTHREAD_MUTEX_NORMAL#import
pthread_mutex (recursive) 递归锁 PTHREAD_MUTEX_RECURSIVE#import
pthread_mutex (cond) 条件锁 pthread_cond_t#import
pthread_rwlock 读写锁 读操作重入,写操作互斥
@synchronized 互斥锁 性能差,且无法锁住内存地址更改的对象
NSLock 互斥锁 封装 pthread_mutex
NSRecursiveLock 递归锁 封装 pthread_mutex (recursive)
NSCondition 条件锁 封装 pthread_mutex (cond)
NSConditionLock 条件锁 可以指定具体条件值

9、NSOperationQueue中的maxConcurrentOperationCount默认值

-1。这个值使系统根据系统条件而设置最大值

10、NSTimer、CADisplayLink、dispatch_source_t 的优劣

优点 缺点
NSTimer 使用简单 依赖 Runloop,具体表现在 无 Runloop 无法使用、NSRunLoopCommonModes、不精确 加入到 主线程
CADisplaylink 依赖屏幕刷新频率出发事件,最精确。最合适做 UI 刷新。 若屏幕刷新被影响,事件也被影响、事件触发的时间间隔只能是屏幕刷新 duration 的倍数、若事件所需时间大于触发事件,跳过数次、不能被继承
dispatch_source_t 不依赖 Runloop 并不精确,使用相对麻烦
  • NSTimer

    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    [timer invalidate];
    
    
  • CADisplaylink

    CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(takeTimer:)];
    [link addToRunLoop:[NSRunLodop currentRunLoop] forMode:NSRunLoopCommonModes];
    link.paused = !link.paused;
    [link invalidate];
    
    
  • dispatch_source_t : 具体查看 2.10 dispatch_source

    __block int countDown = 6;
    
    /// 创建 计时器类型 的 Dispatch Source
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    /// 配置这个timer
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
    /// 设置 timer 的事件处理
    dispatch_source_set_event_handler(timer, ^{
        //定时器触发时执行
        if (countDown <= 0) {
            dispatch_source_cancel(timer);
    
            NSLog(@"倒计时 结束 ~~~");
        }
        else {
            NSLog(@"倒计时还剩 %d 秒...", countDown);
        }
    
        countDown--;
    });
    
    /// 启动 timer
    dispatch_resume(timer);
    
    

搞事情~~~

面试资料:

面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431
面试题资料或者相关学习资料都在群文件中 进群即可下载!

一、 NSThread

NSThread 是苹果封装过的,面向对象。可以使用它直接操作线程,但需要开发者手动管理其生命周期。

但是相比于 GCD 与 NSOperation / NSOperationQueue 来说更加轻量。

1 创建 NSThread

在 iOS 10之前:

// 创建 NSThread
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething) object:nil];
// 启动 thread
[thread start];

// 创建并启动线程 - ;类方法
[NSThread detachNewThreadSelector:@selector(doSomething) toTarget:self withObject:nil];
复制代码

在 iOS 10之后,苹果贴心地为我们准备了 Block 的回调方式:

- (instancetype)initWithBlock:(void (^)(void))block;

+ (void)detachNewThreadWithBlock:(void (^)(void))block;
复制代码

除了显示创建线程实例之外,Apple 还为我们提供多种 NSObject 的分类方法来使用,具体函数详见 NSObject - Objective-C Runtime 分类 Sending Messages

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第1张图片

2 NSThread 常用方法

NSThread 的方法虽说不多,但其实也不少

  • 常用的类方法与类属性:
// 获取当前线程,只读类属性
@property (class, readonly, strong) NSThread *currentThread;

// 若 number 为 1,则证明为主线程
// {number = 1, name = main}

// 获取主线程,只读类属性
@property (class, readonly, strong) NSThread *mainThread;

// 判断当前线程是否是主线程,只读类属性
@property (class, readonly) BOOL isMainThread;

// 休眠一定时间
+ (void)sleepUntilDate:(NSDate *)date;

// 休眠到特定时间
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

// 退出当前线程
+ (void)exit;

  • 常用的实例方法与实例属性:
// 线程名
@property (nullable, copy) NSString *name;

// 是否正在执行任务
@property (readonly, getter=isExecuting) BOOL executing;
// 是否已执行结束
@property (readonly, getter=isFinished) BOOL finished;
// 是否已被取消(一旦被取消,则该线程应 exit)
@property (readonly, getter=isCancelled) BOOL cancelled;

// 是否为主线程
@property (readonly) BOOL isMainThread;

// 取消线程(一旦取消,则该线程应 exit)
- (void)cancel;

// 启动线程
- (void)start;

二、GCD

通常情况,系统都会允许应用提交异步请求,然后系统处理请求的过程中,应用可以继续处理自己的事情。

GCD 便是基于这个准则而设计。

Dispatch - Apple 中这样介绍 GCD:

Execute code concurrently on multicore hardware by submitting work to dispatch queues managed by the system.

通过向系统管理的 dispatch queues 提交工作来在多核硬件上并发执行代码。

GCD,是 iOS 中多线程编程使用最多也是最方便的解决方案。

使用 GCD 有如下好处:

  • GCD 会自动使用更多的 CPU 内核;
  • GCD 自动管理线程的生命周期;
  • GCD 能通过延迟昂贵计算任务并在后台运行来改善应用的相应性能;
  • GCD 提供了一个易于使用的并发模型(不仅仅是线程与锁);
  • 开发者只需要告诉 GCD 该干什么,无需多余的线程管理代码;

1、任务与队列

GCD 中有两个重要概念:任务队列

1.1 任务

执行的操作,也就是使用 GCD 时 Block 中需要执行的那段代码。

我个人理解,任何一句代码都是一个任务。比如 int a = 1 或者 NSLog(@"log");

执行任务有两种方式:同步执行(dispatch_sync异步执行(dispatch_async。两者区别在于是否会阻塞当前线程以及是否具有开启新线程的能力。

  • 同步执行:阻塞当前线程并等待 Block 中的任务执行完成,然后当前线程才会继续往后执行。不具备开启新线程的能力。

  • 异步执行:不阻塞当前线程,当前线程直接往后执行。具备开启新线程的能力,但不一定会开启新线程。

1.2 队列

存放任务的队列。队列是一种特殊的线性表,采用先进先出(FIFO)的规则。

也就是说,新加入的任务总是被插入到队列的末尾,但执行任务是从队列头开始的。这就跟日常生活中的排队一样。

队列分为 串行队列(Serial Dispatch Queue)并行队列(Concurrent Dispatch Queue)

  • 串行队列中的任务按照 FIFO 的顺序取出并执行,前一个任务执行完才会取出下一个。

  • 并行队列中的任务也是按照 FIFO 的顺序取出,但是 GCD 会开启新的线程来执行取出的任务。

    这个取出任务并开启新线程执行的动作非常快,所以看起来就像是任务一起执行的。

    但是,如果队列中的任务数量过大,GCD 也不可能开启一万条线程同时执行任务的。

    同时,并对队列的并发功能只在 异步执行 时有效。

串行队列与并行队列的区别可以使用 这篇博客 的两张图来说明:

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第2张图片

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第3张图片

GCD 公开有五中不同的队列:主线程的 main queue,3个不同优先级的后台队列,一个优先级更低的后台队列(用于 I/O)

同时,用户还可以创建自定义队列,串行队列或并行队列都可以。在自定义队列中被调度的所有 Block 最终都将放入到系统的全局队列和线程池中。

复制一张 大佬的图

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第4张图片
同步执行 异步执行
串行队列 当前线程,一个一个执行 其他线程,一个一个执行
并行队列 当前线程,一个一个执行 开很多线程,同时执行

2、使用 GCD

都说 GCD 简单易用,那就来用一下:

  1. 先创建一个队列(或者获取系统的全局队列);
  2. 将任务追加到队列中。

完了,然后系统就会根据任务类型和队列来执行任务(到底是同步执行,还是异步执行,在那个队列执行)。

2.1 创建队列

主队列(Main Dispatch Queue)

主队列,一个特殊的 串行队列。所有放到主队列的任务都会放到主线程执行。主要用于刷新 UI,当然,你也可以把任何操作都放到主队列中。

原则上来说任何刷新 UI 的操作都应该放到主队列执行,而耗时操作尽量放到其他线程执行。

主队列无法创建,只能获取。

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

全局队列(Global Dispatch Queue)

全局队列,苹果提供给开发者可以直接使用的全局并发队列。

一些与 UI 无关的操作应该放到全局队列来执行,而不是主队列。比如网络请求这类操作。

通过 GCD 提供的 dispatch_get_global_queue 方法获取全局队列:

/*
 * @function: dispatch_get_global_queue
 * @abstract: 获取全局队列
 * @para identifier
        队列优先级,一般使用 DISPATCH_QUEUE_PRIORITY_DEFAULT。
 * @para flags
        保留参数,传 0。传递除零以外的任何值都可能导致返回值为 NULL。
 * @result: 返回指定的队列,若失败则返回 NULL
 */
dispatch_queue_global_t 
dispatch_get_global_queue(long identifier, unsigned long flags);

dispatch_get_global_queue 第一个参数 identifier 有如下选择:


/*
 * - DISPATCH_QUEUE_PRIORITY_HIGH
 * - DISPATCH_QUEUE_PRIORITY_DEFAULT
 * - DISPATCH_QUEUE_PRIORITY_LOW
 * - DISPATCH_QUEUE_PRIORITY_BACKGROUND
 */

/// 派发到此队列的任务将以最高优先级执行
/// 此队列的任务将会被安排到默认优先级及低优先级的任务之前执行
#define DISPATCH_QUEUE_PRIORITY_HIGH 2

/// 派发到此队列的任务将以默认优先级执行 
/// 此队列的任务将会被安排在 “所有高优先级任务全部调度完成之后,低优先级任务被调度之前” 调度执行
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0

/// 派发到此队列的任务将以低优先级执行
/// 此队列的任务将会被安排在 “所有高优先级和默认优先级的任务全度调度完成之后” 调度执行
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)

/// 派发到此队列的任务将以后台优先级执行
/// 此队列的任务将会被安排在所有高优先级任务之后,才会被调度执行。系统将在具有后台状态的线程(setThreadPriority)上运行该队列上的任务,
/// (磁盘 I/O 收到限制,线程的调度优先级被设置为最低值)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN

获取 默认优先级 的全局队列

// 获取 默认优先级 的全局队列
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

自 iOS 8 开始,苹果还引入了线程的服务质量 qos_class

/*
 * 苹果建议我们使用服务质量类别的值来标记全局并发队列
 *  - QOS_CLASS_USER_INTERACTIVE
 *  - QOS_CLASS_USER_INITIATED
 *  - QOS_CLASS_DEFAULT
 *  - QOS_CLASS_UTILITY
 *  - QOS_CLASS_BACKGROUND
 *
 * 全局并发队列仍然可以通过优先级来识别,它会被映射到以下QOS类:
 *  - DISPATCH_QUEUE_PRIORITY_HIGH:         QOS_CLASS_USER_INITIATED
 *  - DISPATCH_QUEUE_PRIORITY_DEFAULT:      QOS_CLASS_DEFAULT
 *  - DISPATCH_QUEUE_PRIORITY_LOW:          QOS_CLASS_UTILITY
 *  - DISPATCH_QUEUE_PRIORITY_BACKGROUND:   QOS_CLASS_BACKGROUND
 */

/*
 * @constant QOS_CLASS_USER_INTERACTIVE
 * @abstract 这个 QOS 类表明该线程执行与用户交互的工作。
 * @discussion 与系统的其他工作相比,这些工作被要求以最高优先级运行。
 * 指定这个 QOS 类将会请求几乎所有可以用的系统 CPU 资源和 I/O 带宽运行,甚至不惜争夺资源。
 * 这不是一个适合大型任务的节能 QOS 类。这个类应该仅限于与用户关键交互。
 * 例如处理主循环的事件,绘图,动画等。
 *
 * @constant QOS_CLASS_USER_INITIATED
 * @abstract 这个 QOS 类表明该线程执行用户发起并可能在等待结果的工作。
 * @discussion 这类工作的优先级低于用户关键交互,但又高于系统上的其他操作。
 * 这不是一个适合大型任务的节能 QOS 类。它的使用应该被限制在极短的时间内,从而用户不至于在等待期间切换任务。
 * 典型的用户发起的通过显示占位符或模态展示用户界面来指示进度的工作。
 * 
 *
 * @constant QOS_CLASS_DEFAULT
 * @abstrct 系统在缺少具体 QOS 类信息的情况下使用的默认 QOS 类。
 * @discussion 这类工作优先级低于用户关键操作和用户发起的工作,但高于实用工具和后台任务。
 * 通过 pthread_create 创建且没有指定 QOS 类属性的线程将默认为 QOS_CLASS_DEFAULT。
 * 这个 QOS 类并不打算作为工作分类,它应该只作为系统提供给传播或恢复的 QOS 类的值。
 *
 *
 * @constant QOS_CLASS_UTILITY
 * @abstract 这个 QOS 类表明该线程执行的工作可能不由用户发起,且用户不期待立即等待结果。
 * @discussion 这类工作优先级低于用户关键操作和用户发起的工作,但高于低级别的系统维护工作。
 * 这个 QOS 类指明这类工作应该以节能高效方式运行。
 * 这种实用工具的工作可能不表明给用户,但是这类工作的影响是用户可见的。
 *
 *
 * @constant QOS_CLASS_BACKGROUND
 * @abstract 这个 QOS 类表明该线程执行的工作不由用户发起,且用户可能并不知道结果。
 * @discussion 这类工作优先级低于其他工作。
 * 这个 QOS 类指明这类工作应该以最节能高效的方式运行。
 *
 *
 * @constant QOS_CLASS_UNSPECIFIED
 * @abstract 这是一个指示 QOS 类信息缺失或者被移除的标记。
 * @discussion 作为 API 返回值,可能表示线程或 pthread 被不兼容的遗留 API 配置,或与 QOS 类系统冲突。
 */
__QOS_ENUM(qos_class, unsigned int,
    QOS_CLASS_USER_INTERACTIVE = 0x21,
    QOS_CLASS_USER_INITIATED = 0x19,
    QOS_CLASS_DEFAULT = 0x15,
    QOS_CLASS_UTILITY = 0x11,
    QOS_CLASS_BACKGROUND = 0x09,
    QOS_CLASS_UNSPECIFIED = 0x00,
);

面试资料:

面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431

自定义队列

苹果允许开发者创建自定义队列,串行队列和并行队列都可以创建。

/*
 * @funcion: dispatch_queue_create
 * @abstract: 创建自定义队列
 * @para label
        队列标签,可以为 NULL。
 * @para attr
        队列类型,串行队列还是并行队列,DISPATCH_QUEUE_SERIAL 与 NULL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并行队列。
 * @result: 返回创建好的队列。
 */
dispatch_queue_t
dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);

如注释所说

  • 第一个参数为队列标签,可为 NULL,开发者可是用这个值来方便地 DEBUG。队列标签推荐使用应用程序 ID 这种的逆序域名。

  • 第二个参数就比较重要了,它表明开发者需要创建的队列类型,串行队列还是并行队列。

    串行队列: DISPATCH_QUEUE_SERIALNULL 。 并行队列: DISPATCH_QUEUE_CONCURRENT

2.2 创建任务

搞了这么半天其实都是准备工作,只是为了创建一个可以存放任务的容器。只不过这个容器不可或缺。

/*
 * @function: dispatch_sync
 * @abstract: 在当前线程同步执行任务,会阻塞当前线程直到这个任务完成。
 * @para queue
        队列。开发者可以指定在哪个队列执行这个任务
 * @para block
        任务。开发者在这个 Block 内执行具体任务。
 * @result: 无返回值
 */
//void
//dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"同步执行");
});

/*
 * @function: dispatch_async
 * @abstract: 另开线程异步执行任务,不会阻塞当前线程。
 * @para queue
        队列。开发者可以指定在哪个队列执行这个任务
 * @para block
        任务。开发者在这个 Block 内执行具体任务。
 * @result: 无返回值
 */
// void
// dispatch_async(dispatch_queue_t queue,
        DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"异步执行");
});

如注释所说,第一个参数表明放在哪个队列执行,第二个表明任务是什么。

同步与异步最大的区别就在于是否会阻塞当前线程:

  • dispatch_sync 会阻塞当前线程。
  • dispatch_async 不会阻塞当前线程。

2.3 组合任务与队列

在当前线程为主线程的情况下,任务执行方式与队列种类两两组合一下:

  1. 同步执行 + 串行队列
  2. 同步执行 + 并行队列
  3. 异步执行 + 串行队列
  4. 异步执行 + 串行队列

对了,还忘了两个,主队列

  1. 同步执行 + 主队列
  2. 异步执行 + 主队列

在当前线程为主线程的情况下:

串行队列 并行队列 主队列
同步执行 不开启新线程,串行执行任务 不开启新线程,串行执行任务 死锁
异步执行 开启一条新线程,串行执行任务 开启新线程(可能会有多条),并发执行任务 不开启新线程,串行执行任务

说人话:

  • 同步执行,在当前线程执行指定任务,而且会阻塞当前线程的后续任务;

    在同步执行条件下,并行队列也无法并行,毕竟阻塞了。

    注意:同步执行 + 主队列 = 死锁

  • 异步执行不需要阻塞,开启新线程执行任务,且不阻塞当前线程的后续任务。

    在异步执行条件下,串行队列与并行队列都会开启新线程

    只不过串行队列值开启一条新线程,并行队列会尽量开启多的线程来分别执行任务(毕竟有上限,不可能同时开启1000000条)。

  • 主队列是个串行队列,且只能选择异步执行,毕竟 同步执行 + 主队列 = 死锁

验证一下 同步执行 + 串行队列 = 死锁

/**
 * 同步执行 + 主队列
 */
- (void)syncMain {

    NSLog(@"当前线程:%@", [NSThread currentThread]);
    NSLog(@"syncMain --- 开始");

    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"同步执行 + 主队列");
    });

    NSLog(@"syncMain --- 结束");
}

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第5张图片

程序崩溃了。仅打印出 “当前线程信息” 以及 “syncMain --- 开始”,然后便没有然后了。

先收集当前执行环境:

  1. 主线程执行;
  2. 同步执行;
  3. 主队列任务、

我在上边说过,任何一句代码都是一个任务。

很明显,程序崩溃的时候,正在执行 dispatch_sync 任务(称之为 任务1),而 任务1 的内容是 停止当前工作,立即执行 ^{ NSLog(@"同步执行 + 主队列"); } (称之为 任务2)。

dispatch_sync 会阻塞当前线程,具体是 【在执行完 Block 之前,dispatch_sync 不会 return】。这就意味着 直到完成 任务2dispatch_sync 才能 return。

说人话,主线程一直在执行 任务1,除非 任务2 完成。

但是我们是使用 主队列 来执行这里同步操作的,主队列如果要执行下一个任务,那么当前任务必须完成。

此时,任务1 等待任务完成,自己才能完成;而 任务2 等待 任务1 完成,自己才能开始执行。

是不是跟死锁的机制一模一样:我在等着你,而你也在等着我。

没错!!!这里就是死锁,不过新版 GCD 加入了死锁检测机制,如果发生死锁,则会引发 crash。

有兴趣的朋友可以去 Apple 开源代码 - libdispatch 或 GCD源码吐血分析(2)

事实上,并不是 同步执行 + 主队列 = 死锁,而是 在主线程环境下 + 同步执行 + 主队列 = 死锁

在上升一层, 一个串行队列的任务正在被执行,若此时给这条串行队列同步提交任务时,则会引发 crash

证明一下:


阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第6张图片
阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第7张图片
阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第8张图片
阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第9张图片

至于 GCD源码吐血分析(2) 里说的 可以绕开 crash 来引发死锁,我想我可能做到了。。。(感兴趣的朋友可以试试下面这段代码)

- (void)theDeadLock {

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

    dispatch_queue_t theQueue = dispatch_queue_create("com.junes.serialQueue", DISPATCH_QUEUE_SERIAL);

    /// 如果是在 主线程中执行,那这里一定要异步,否则 第二层直接凉凉
    /// 如果在其他线程,那么这里同步异步没有关系
    dispatch_async(theQueue, ^{ /// 第一层
        NSLog(@"1 %@", [NSThread currentThread]);

        /// 这里一定要 同步执行
        /// 如果这里提前 return了,那么 theQueue 中将暂时没有任务
        /// 即可以立即执行 第三层任务,就不符合死锁条件
        dispatch_sync(dispatch_get_main_queue(), ^{     /// 第二层
            NSLog(@"2 %@", [NSThread currentThread]);

                NSLog(@"奥利给 %@", [NSThread currentThread]);
            });

            NSLog(@"3 %@", [NSThread currentThread]);
        });

        NSLog(@"4 %@", [NSThread currentThread]);
    });

    NSLog(@"5 %@", [NSThread currentThread]);
}

  • 这里触发死锁的原理:执行第三层时,theQueue 必须不能为空。

    第二层不能在 第三层完成之前 return,否在 theQueue 中没有任务,那完全可以立即执行 第三层的任务。

  • 能避开 crash 的原理:我也不清楚。。。。

    反正把 第二层的主队列 换成 全局并行队列或者自定义串行队列都会直接引发 crash。。。

    如果有大佬知道原理,求告知。。。拜谢

至于在 队列中嵌套队列 ,这里也给一个表格(【】代表外层操作,并且所有外层操作都能正常运行前提下):

【同步执行 + 串行队列】嵌套同一个串行队列 【同步执行 + 并行队列】嵌套同一个并行队列 【异步执行 + 串行队列】嵌套同一个串行队列 【异步执行 + 并行队列】嵌套同一个并行队列
同步 死锁 当前线程串行执行 死锁 当前线程串行执行
异步 另开线程(1条)串行执行 另开线程并行执行 另开线程(1条)串行执行 另开线程并行执行

2.4 延迟执行 dispatch_after

需求:延后一段时间再执行任务。

  • dispatch_after 方法可以实现延时执行任务。

其参数为:

  1. when:再过多久将任务提交至队列;
  2. queue:提交到哪个队列;
  3. block:提交什么任务。

dispatch_afterNSTimer 优秀,因为他不需要指定 Runloop 的运行模式。 dispatch_afterNSObject.performSelector:withObject:afterDelay: 优秀,因为它不需要 Runloop 支持。

NSLog(@"开始执行 dispatch_after");

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"三秒后");
});

但是请注意,dispatch_after 并不是在指定时间后执行任务,而是在指定时间之后才将任务提交到队列中。

所以,这个延迟的时间是不精确的。这是缺点之一。

第二个缺点便是,dispatch_after 延后执行的 Block 无法直接取消。但是 Dispatch-Cancel 提供了一种解决方案。

其具体实现可以在 Apple 开源代码 - libdispatchd > Dispatch Source > source.c 中查看,这里就不细说了:

判断 when,如果是现在,异步执行它;否则就创建一个 dispatch source 以便在指定时间触发 dispatch_async

2.5 单次执行 dispatch_once

需求:单例模式。

  • dispatch_once 允许开发者在线程安全地执行且只执行一次指定任务。这非常适合单例模式.

其参数为:

  1. predicate:单次执行的标记;
  2. block:需要单次执行的任务。
static TheClass *instance = nil;
+ (instance)sharedInstance 
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[TheClass alloc] init];
    });

    return instance;
}

GCD 是线程安全的。任何试图访问临界区(即传递给 dispatch_once 的任务)的线程会在临界区已有一个线程的情况下被阻塞,直到临界区线程完成操作。

遗憾的是,Swift 取消了 dispatch_once 这个操作,毕竟在 Swift 中实现单例实在是太简单了(只需要将初始化方法设置为私有,然后提供一个静态实例变量即可)。

这里提供一个 Swift 版的 dispatch_once

// MARK: - DispatchQueue once
extension DispatchQueue {

    private static var _onceTracker = [String]()

    /**
     Executes a block code, associated with a unique token, only once.
     The clode is thread safe and will only execute the code once even in
     the prescence of multithread calls.

     - parameter token: A unique reverse DNS style name suce as com.vectorfrom. or a GUID
     - parameter block: Block to execute once
     **/
    public class func once(token: String, block: () -> Void) {
        objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }

        guard !_onceTracker.contains(token) else { return }
        _onceTracker.append(token)

        block()
    }
}

2.6 并发迭代 dispatch_apply

需求: 遍历一个很大很大的集合,使用 for 循环将会花费很多很多事件。

  • dispatch_apply 按照指定的次数将指定的任务提交到指定的队列中,同步执行并等待所有任务完成后 return。

说人话,dispatch_apply 就是一个高级一些的 for 循环,它支持并发迭代。并且它 是同步执行的,必须等到所有工作完成才能返回,这与 for 循环一样。

其参数为:

  1. iterations:需要迭代的次数;
  2. queue:将迭代任务提交到哪个队列;
  3. block:具体的迭代任务是什么。
dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
    NSLog(@"dispatch_apply --- %zu --- %@", index, [NSThread currentThread]);
});

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第10张图片

上边的例子并不值得使用 dispatch_apply:创建并运行线程是需要付出代价的(时间开销,内存开销)。

针对简单的迭代,使用 for 循环远比 dispatch_apply 实惠。如果需要迭代非常大的集合,才应该考虑使用 dispatch_apply

dispatch_apply 在各队列上的表现(当前为主线程):

  • 主队列:死锁(毕竟这是同步执行);
  • 串行队列:串行队列会完全抵消 dispatch_apply 并行迭代的功能,还不如 for 循环;
  • 并行队列:并行执行迭代任务,这是非常好的选择,也是 dispatch_apply 的意义所在。

2.7 栅栏方法 dispatch_barrier_async

需求:异步执行两组任务,但第二组任务需要第一组完成之后才能执行。

  • dispatch_barrier_async 可以提供一个 “栅栏” 将两组异步执行的任务分隔开,保证先于栅栏方法提交到队列的任务全部执行完成之后,然后开始执行将栅栏任务,等到栅栏任务执行完成后,该队列便恢复原本执行状态。

其参数为:

  1. queue:需要隔开的任务所在的队列;
  2. block:栅栏任务的具体内容。
- (void)barrier_display {

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

    dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_queue_t theQueue = theConcurrentQueue;

    dispatch_async(theQueue, ^{
        NSLog(@"任务1 开始");
        // 模拟耗时任务
        [NSThread sleepForTimeInterval:2];

        NSLog(@"任务1 完成");
    });

    dispatch_async(theQueue, ^{
        NSLog(@"任务2 开始");
        // 模拟耗时任务
        [NSThread sleepForTimeInterval:1];

        NSLog(@"任务2 完成");
    });

    dispatch_barrier_async(theQueue, ^{
        NSLog(@"==================  栅栏任务 ==================");
    });

    dispatch_async(theQueue, ^{
        NSLog(@"任务3 开始");
        // 模拟耗时任务
        [NSThread sleepForTimeInterval:4];

        NSLog(@"任务3 完成");
    });
    dispatch_async(theQueue, ^{
        NSLog(@"任务4 开始");
        // 模拟耗时任务
        [NSThread sleepForTimeInterval:3];

        NSLog(@"任务4 完成");
    });
}

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第11张图片

看看 dispatch_barrier_asyncdispatch_async

查看源码:

  • dispatch_barrier_async

    void dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work)
    {
        dispatch_continuation_t dc = _dispatch_continuation_alloc();
        uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;
        dispatch_qos_t qos;
    
        qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
        _dispatch_continuation_async(dq, dc, qos, dc_flags);
    }
    
    
  • dispatch_async

    void dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
    {
        dispatch_continuation_t dc = _dispatch_continuation_alloc();
        uintptr_t dc_flags = DC_FLAG_CONSUME;
        dispatch_qos_t qos;
    
        qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
        _dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
    }
    
    

可以发现,dispatch_barrier_asyncdispatch_async 机会一模一样,唯一的区别就在于

/// 这是 dispatch_barrier_async
uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;

/// 这是 dispatch_async
uintptr_t dc_flags = DC_FLAG_CONSUME;

两者唯一的区别在于 创建 dispatch_qos_t qos 传入的 dc_flags

dispatch_barrier_asyncdispatch_async 多了一个标记 DC_FLAG_BARRIER

而这个标记对全局并发队列不起作用。。。。

看看dispatch_barrier_syncdispatch_sync

至于 dispatch_barrier_syncdispatch_sync。查看源码:

  • dispatch_barrier_sync

    void dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work)
    {
        uintptr_t dc_flags = DC_FLAG_BARRIER | DC_FLAG_BLOCK;
        if (unlikely(_dispatch_block_has_private_data(work))) {
            return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
        }
        _dispatch_barrier_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
    }
    
    static void _dispatch_barrier_sync_f(dispatch_queue_t dq, void *ctxt,
            dispatch_function_t func, uintptr_t dc_flags)
    {
        _dispatch_barrier_sync_f_inline(dq, ctxt, func, dc_flags);
    }
    
    
  • dispatch_sync

    void dispatch_sync(dispatch_queue_t dq, dispatch_block_t work)
    {
        uintptr_t dc_flags = DC_FLAG_BLOCK;
        if (unlikely(_dispatch_block_has_private_data(work))) {
            return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
        }
        _dispatch_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
    }
    
    static void _dispatch_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func,
            uintptr_t dc_flags)
    {
        _dispatch_sync_f_inline(dq, ctxt, func, dc_flags);
    }
    
    static inline void _dispatch_sync_f_inline(dispatch_queue_t dq, void *ctxt,
            dispatch_function_t func, uintptr_t dc_flags)
    {
        if (likely(dq->dq_width == 1)) {
            /// 串行队列执行到这里
            return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags);
        }
    
        if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
            DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
        }
    
        dispatch_lane_t dl = upcast(dq)._dl;
        // Global concurrent queues and queues bound to non-dispatch threads
        // always fall into the slow case, see DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
        /// 通过下面的堆栈,当创建全局并行队列的时候,才会执行到此方法
        if (unlikely(!_dispatch_queue_try_reserve_sync_width(dl))) {
            return _dispatch_sync_f_slow(dl, ctxt, func, 0, dl, dc_flags);
        }
    
        if (unlikely(dq->do_targetq->do_targetq)) {
            return _dispatch_sync_recurse(dl, ctxt, func, dc_flags);
        }
        _dispatch_introspection_sync_begin(dl);
        /// 执行 Block
        _dispatch_sync_invoke_and_complete(dl, ctxt, func DISPATCH_TRACE_ARG(
                _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags)));
    }
    
    

没看明白???

那就看看堆栈:

  • dispatch_barrier_sync

    dispatch_barrier_sync_dispatch_barrier_sync_f

  • dispatch_sync

    dispatch_sync_dispatch_sync_f_dispatch_sync_f_inline_dispatch_barrier_sync_f

如果是串行队列,最后调用到了同一个方法 _dispatch_barrier_sync_f

栅栏方法在在各队列上的表现(当前为主线程):

主队列 自定义串行队列 全局并行队列 自定义并行队列
dispatch_barrier_async 串行队列毫无意义 串行队列毫无意义 相当于 dispatch_async,无法达成栅栏目的 在之前和之后的任务之间加一道栅栏,栅栏任务在之前的所有任务完成之后开始执行,完成之后恢复队列原本的工作状态
dispatch_barrier_sync 死锁 串行执行任务

在当前为主线程环境下,一个个验证(串行队列就没必要验证了):

  • dispatch_barrier_async + 自定义并行队列
    阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第12张图片
完美符合需求,不是吗?

**自定义并发队列** 是 `dispatch_barrier_async` 最佳拍档。
  • dispatch_barrier_async + 全局并行队列
阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第13张图片
完全跟 `dispatch_async` 相同,根本无法达成栅栏目的。

全局并发队列,可能也会被系统系统使用,请不要为了栅栏而垄断它。
  • dispatch_barrier_sync + 主队列

    死锁,上边的源码解读能找到原因。

  • dispatch_barrier_sync + 自定义串行队列

    串行队列有啥好加栅栏的。。。况且 dispatch_barrier_sync 还会阻塞线程。

  • dispatch_barrier_sync + 全局并行队列

    dispatch_barrier_async + 全局并行队列类似,毫无栅栏效果。。。

  • dispatch_barrier_sync + 自定义并行队列

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第14张图片
也是符合要求的,不是吗?

结论:自定义并行队列 是栅栏方法的好帮手

2.8 调度组 dispatch_group_t

需求:分别异步执行几个耗时任务,然后当几个耗时任务都执行完毕后再回到主线程执行任务。

初步看到这个任务,刚才的栅栏任务也能做到嘛,有必要花时间了解一个新东西吗?且往下看。。。

  • Dispatch Group 会在整个组的任务全部完成时通知开发者。这些任务可以使同步的,也可以是异步的,甚至可以再不同队列。

要监控如此分散的任务的执行情况,这会让开发者头疼痛不已。幸好有调度组 dispatch_gourp_t来帮开发者记下这些不同的任务。

  • 创建调度组
/// 创建调度组
dispatch_group_t = dispatch_group_create();

  • 将任务放进调度组

创建完调度组之后,需要将任务放进调度组中。有两种方式都可以完成这个工作,但是其侧重点不同:

  1. dispatch_group_async

    异步请求。任务自动完成,其内部代码执行完毕即视为任务完成。

    网络请求一般也是异步请求。所以只要请求发送完成即视为任务完成,但其实任务并没有真正完成

    适合内部任务为同步完成的,比如处理一个非常大的集合,或者计算量很大的任务。

  2. dispatch_group_enter

    通知调度组有一个任务开始执行了。任务并不会自动完成,需要我们使用 dispatch_group_leave 来告诉调度组有一个任务完成了。

    适合内部任务为异步完成的,比如异步的网络请求、文件下载。

    但是 dispatch_group_enter 必须与 dispatch_group_leave 成对出现,否则可能会出现崩溃。

先来验证一下以上说的适不适合的问题,同时也演示一下用法。

- (void)group_validate {

    /// 创建一个调度组
    dispatch_group_t group = dispatch_group_create();

    dispatch_queue_t theGlobalQueue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t theSerialQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);

    /// 将任务丢进调度组
    dispatch_group_async(group, theGlobalQueue, ^{
        NSLog(@"任务1 开始 +++++++");

        /// 模拟耗时操作
        sleep(2);

        NSLog(@"任务1 完成 -----------------");
    });

    dispatch_group_async(group, theSerialQueue, ^{
        NSLog(@"任务2 开始 +++++++");

        /// 模拟耗时操作
        sleep(4);

        NSLog(@"任务2 完成 -----------------");
    });

    dispatch_group_async(group, theConcurrentQueue, ^{
        NSLog(@"任务3 开始 +++++++");

        /// 模拟异步网络请求
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(5);

            NSLog(@"任务3 现在才真正完成 -----------------");
        });

        NSLog(@"任务3 现在被 dispatch_group_notify 已经完成了");
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"所有任务都完成了。。。");
    });

}

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第15张图片

结果图的红框部分可以证明刚才的理论。

接下来,将“异步的网络操作” 改用 dispatch_group_enter 来放入调度组再看一下。

- (void)group_display {

    /// 创建一个调度组
    dispatch_group_t group = dispatch_group_create();

    dispatch_queue_t theGlobalQueue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t theSerialQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);

    /// 将任务丢进调度组
    dispatch_group_async(group, theGlobalQueue, ^{
        NSLog(@"任务1 开始 +++++++");

        /// 模拟耗时操作
        sleep(2);

        NSLog(@"任务1 完成 -----------------");
    });

    dispatch_group_async(group, theSerialQueue, ^{
        NSLog(@"任务2 开始 +++++++");

        /// 模拟耗时操作
        sleep(4);

        NSLog(@"任务2 完成 -----------------");
    });

    dispatch_group_enter(group);
    /// 模拟异步网络请求
    dispatch_async(theConcurrentQueue, ^{
        NSLog(@"任务3 开始 +++++++");

        sleep(5);

        NSLog(@"任务3 完成 -----------------");
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"所有任务都完成了。。。");
    });

    NSLog(@"dispatch_group_notify 为异步执行,并不会阻塞线程。我就是证据");
}

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第16张图片

面试资料:

面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431

这才是使用 调度组 的正常操作!

  • 任务完成
  1. dispatch_group_notify

    异步执行。指定的调度组内任务全部完成之后,将 Block 加入到特定队列。

    对于 dispatch_group_async 的任务,只要其 Block 代码执行完成即认为任务已完成。(无论其 Block 内是否还有异步请求,这一点在上边已经验证过了)

    对于 dispatch_group_enter 的任务,必须使用 dispatch_group_leave 来通知调度组本任务已经完成。

  2. dispatch_group_wait

    同步执行,会阻塞线程。

    在所有任务完成(或者超时)之前,该方法会一直阻塞线程。

上边已经演示了 dispatch_group_notify 的使用。接下来看一下 dispatch_group_wait 的用法。

- (void)group_display {

    /// 创建一个调度组
    dispatch_group_t group = dispatch_group_create();

    dispatch_queue_t theGlobalQueue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t theSerialQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t theConcurrentQueue = dispatch_queue_create("com.junes.serial.queue", DISPATCH_QUEUE_CONCURRENT);

    /// 将任务丢进调度组
    dispatch_group_async(group, theGlobalQueue, ^{
        NSLog(@"任务1 开始 +++++++");

        /// 模拟耗时操作
        sleep(2);

        NSLog(@"任务1 完成 -----------------");
    });

    dispatch_group_async(group, theSerialQueue, ^{
        NSLog(@"任务2 开始 +++++++");

        /// 模拟耗时操作
        sleep(4);

        NSLog(@"任务2 完成 -----------------");
    });

    dispatch_group_enter(group);
    /// 模拟异步网络请求
    dispatch_async(theConcurrentQueue, ^{
        NSLog(@"任务3 开始 +++++++");

        sleep(5);

        NSLog(@"任务3 完成 -----------------");
        dispatch_group_leave(group);
    });

    NSLog(@"dispatch_group_wait 即将囚禁线程");

    /// 传入指定调度组,与超时时间(DISPATCH_TIME_FOREVER 代表永不超时,DISPATCH_TIME_NOW 代表立马超时,完全搞不懂这个有什么用)。
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

    NSLog(@"dispatch_group_wait 释放了线程");

    // 调度组内所有任务都完成了,该做什么就做什么
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"所有任务完成了");
    });
}

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第17张图片

提醒一下dispatch_group_wait 第二个参数指明了何时超时。为了方便,苹果提供了 DISPATCH_TIME_NOWDISPATCH_TIME_FOREVER 两个常量。

  1. DISPATCH_TIME_FOREVER

    永不超时,如果任务一直无法完成,那么线程将一直阻塞。

    如果 dispatch_group_leave 数量少于 dispatch_group_enter ,那结果值得期待。

  2. DISPATCH_TIME_NOW

    立马超时,没有任何异步有机会完成。。。

2.9 信号量 dispatch_semaphore_t

先看一段代码:

__block int theNumber = 0;

/// 创建调度组
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"任务 1 开始了 %@", [NSThread currentThread]);

    for (int i = 0; i < 10000000; ++i) {
        theNumber++;
    }

    NSLog(@"任务 1 完成了 %@", [NSThread currentThread]);
});

dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"任务 2 开始了 %@", [NSThread currentThread]);

    for (int i = 0; i < 10000000; ++i) {
        theNumber++;
    }

    NSLog(@"任务 2 完成了 %@", [NSThread currentThread]);
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"theNumber = %d", theNumber);
});

GCD 来使用多个线程使用同一个资源的例子。最后的执行结果并不是简单的 循环次数 * 2(当然,需要循环次数稍微大一点。。。)

多线程编程时,不可避免地会发生多个线程使用同一个资源的情况。如果没有锁机制,那么就失去了程序的正确性。

为了确保 GCD 编程的正确性,使用资源时(主要是修改资源)必须加锁。

信号量(dispatch_semaphore_t),便是 GCD 的锁机制。意为持有计数的信号,苹果提供了 3 个 API 供开发者使用。

  1. dispatch_semaphore_create

    根据传入的初始值创建一个信号量。

    不可传入负值。运行过程中,若内部值为负数,则这个值的绝对值便是正在等待资源的线程数。

  2. dispatch_semaphore_wait

    信号量 -1。

    -1 之后的结果值小于 0 时,线程阻塞,并以 FIFO 的方式等待资源。

  3. dispatch_semaphore_signal

    信号量 +1。

    +1 之后的结果值大于 0 时,以 FIFO 的方式唤醒等待的线程。

给上边的问题代码加上信号量:

__block int theNumber = 0;

/// 创建信号值为 1 的信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

/// 创建调度组
dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
    /// 信号值 -1
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    NSLog(@"任务 1 开始了 %@", [NSThread currentThread]);

    for (int i = 0; i < 10000000; ++i) {
        theNumber++;
    }

    NSLog(@"任务 1 完成了 %@", [NSThread currentThread]);
    /// 信号值 +1
    dispatch_semaphore_signal(semaphore);
});

dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
    /// 信号值 -1 (此时信号量为负数了,线程阻塞以等待资源)
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    NSLog(@"任务 2 开始了 %@", [NSThread currentThread]);

    for (int i = 0; i < 10000000; ++i) {
        theNumber++;
    }

    NSLog(@"任务 2 完成了 %@", [NSThread currentThread]);
    /// 信号值 +1
    dispatch_semaphore_signal(semaphore);
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"theNumber = %d", theNumber);
});

问题迎刃而解

2.10 调度资源 dispatch_source

dispatch source 是基础数据类型,协调处理特定的底层系统事件。

dispatch source 有以下特征。

  1. 配置一个 dispatch source 时,需要指定监测的事件、dispatch quue以及处理事件的 Block。
  2. 当事件发生时,dispatch source 会将指定的 Block 提交到指定的队列上执行。
  3. 为了防止事件积压到 dispatch queue,dispatch source 采取了事件合并机制。如果新的是时间在上一个事件处理前到达,新旧事件会被合并。根据事件类型的不同,合并操作可能会替换旧事件,或者更新旧事件的信息。
  4. dispatch source 提供连续的事件,除非显示取消,dispatch source 会一直保留与 dispatch queue 的关联。
  5. dispatch source 非常轻量,CPU负荷非常小,几乎不占用资源。它是 BSD 系内核惯有功能 kqueue 的包装,kqueue 是 XUN 内核中发生各种事件时,在应用程序编程执行处理的技术。kqueue 可以称为应用程序处理 XUN 内核中丰盛各种事件的方法中最优秀的一种。

dispatch_source 的种类

/*
 *当同一时间,一个事件的的触发频率很高,那么Dispatch Source会将这些响应以ADD的方式进行累积,然后等系统空闲时最终处理。
 * 如果触发频率比较零散,那么Dispatch Source会将这些事件分别响应。
 */
DISPATCH_SOURCE_TYPE_DATA_ADD        自定义的事件,变量增加
DISPATCH_SOURCE_TYPE_DATA_OR         自定义的事件,变量OR
DISPATCH_SOURCE_TYPE_DATA_REPLACE    自定义的事件,变量Replace
DISPATCH_SOURCE_TYPE_MACH_SEND       MACH端口发送    
DISPATCH_SOURCE_TYPE_MACH_RECV       MACH端口接收 
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE  内存报警
DISPATCH_SOURCE_TYPE_PROC            进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
DISPATCH_SOURCE_TYPE_READ            IO操作,如对文件的操作、socket操作的读响应
DISPATCH_SOURCE_TYPE_SIGNAL          接收到UNIX信号时响应
DISPATCH_SOURCE_TYPE_TIMER           定时器
DISPATCH_SOURCE_TYPE_VNODE           文件状态监听,文件被删除、移动、重命名
DISPATCH_SOURCE_TYPE_WRITE           IO操作,如对文件的操作、socket操作的写响应
DISPATCH_MACH_SEND_DEAD

使用 dispatch source

所有 dispatch source 种类中,最常用的莫过于 DISPATCH_SOURCE_TYPE_TIMER 了。

/*!
 * @abstract:创建指定的 dispatch source
 * @param type
 * 需要创建的 diapatch source 的种类。必须是其种类常量。
 *
 * @param handle
 * 需要监视的基础系统句柄。此参数由 type 参数中提供的常量确定。传 0 即可。
 *
 * @param mask
 * 标志掩码,指定需要哪些事件。此参数由 type 参数中提供的常量确定。传 0 即可。
 *
 * @param queue
 * 在哪个队列处理事件。
 */
dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
    uintptr_t handle,
    unsigned long mask,
    dispatch_queue_t _Nullable queue);

dispatch_source_create 创建的 dispatch source 处于挂起状态,需要手动唤醒。

配置 dispatch source

/*!
 * @abstract
 * 配置这个计时器类型的 dispatch source
 *
 * @param start
 * 何时开始接收事件。更多信息查看  dispatch_time() 和 dispatch_walltime()。
 *
 * @param interval
 * 计时器间隔(纳秒级单位)。使用 DISPATCH_TIME_FOREVER 即代表一次性使用。
 *
 * @param leeway
 * 允许的误差(纳秒级单位)。
 */
void
dispatch_source_set_timer(dispatch_source_t source,
    dispatch_time_t start,
    uint64_t interval,
    uint64_t leeway);   

/*!
 * @abstract
 * 为给定 dispatch source 设定事件处理。
 *
 * @param source
 * 需要配置的 dispatch dispatch。
 *
 * @param handler
 * 收到事件时的处理回调。前者为 Block,后者为函数。
 */
void
dispatch_source_set_event_handler(dispatch_source_t source,
    dispatch_block_t _Nullable handler);
void
dispatch_source_set_event_handler_f(dispatch_source_t source,
    dispatch_function_t _Nullable handler);

dispatch_source_set_event_handler 使用 Block 作为回调。而 dispatch_source_set_event_handler_f 则使用函数指针作为回调。

启动、挂起、取消 dispatch source

/// 唤醒指定 dispatch source
void
dispatch_resume(dispatch_object_t object);

/// 挂起指定 dispatch source。
void
dispatch_suspend(dispatch_object_t object);

/// 取消指定 dispatch source
void
dispatch_source_cancel(dispatch_source_t source);

/// 查看指定 dispatch source 是否已经取消。已取消返回零,否则非零。
long
dispatch_source_testcancel(dispatch_source_t source);

/// 取消 dispatch source 后最后一次事件的处理。
void
dispatch_source_set_cancel_handler(dispatch_source_t source,
    dispatch_block_t _Nullable handler);

关于以上方法,有几下几点需要解释:

  1. 新创建的 dispatch source 处于挂起状态,必须手动调用 dispatch_resume 才能工作;
  2. dispatch source 处于挂起状态时,发生的所事件都会被累积。 dispatch source 被恢复,但是不会一次性传递所有事件,而是先合并到单一事件中;
  3. 取消 dispatch source 是一个异步操作,调用 disaptch_source_cancel 之后,不会再有新的事件被传递,但是正在被处理的事件会被继续处理;
  4. 处理完最后的事件之后, dispatch source 会执行自己的取消处理器(dispatch_source_set_cancel_handler)。在取消处理器中,可以执行内存和资源的释放工作;
  5. 一定要在 dispatch source 正常工作的情况下取消它。在挂起状态千万不要调用 disaptch_source_cancel 取消 dispatch source

好,上一个完整实例:

__block int countDown = 6;

/// 创建 计时器类型 的 Dispatch Source
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
/// 配置这个timer
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
/// 设置 timer 的事件处理
dispatch_source_set_event_handler(timer, ^{
    //定时器触发时执行
    if (countDown <= 0) {
        dispatch_source_cancel(timer);

        NSLog(@"倒计时 结束 ~~~");
    }
    else {
        NSLog(@"倒计时还剩 %d 秒...", countDown);
    }

    countDown--;
});

/// 启动 timer
dispatch_resume(timer);

三、NSOperation 和 NSOperationQueue

NSOperationNSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。两者分别对应 GCD 的任务与队列。相比 GCD,NSOperationNSOperationQueue 更加简单易用,代码可读性也更高,但是系统开销会稍微大一点。

借用 大佬的一张思维导图 来说明相关的知识点:

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第18张图片

3.1 操作 NSOperation

NSOperation 翻译过来就是 “操作”,对应 GCD 中的任务。

NSOperation 是个抽象类,本身无法直接使用,不过 Apple 为我们准备了两个子类:NSInvocationOperationNSBlockOperation。当然,我们也可以自定义子类(AFNetworking 中自定义了 一个子类 AFURLConnectionOperation)。

NSOperation 是一次性的,它的任务只能被执行一次,执行完之后不能再次执行。

NSoperation 有三种重要状态:

  1. isReady:返回 YES 则代表已准备好被执行,否则说明还有一些准备工作还未完成;
  2. isExecuting:返回 YES 则代表正在被执行;
  3. isFinished:返回 YES 则代表已完成(被取消 isCancelled 也被认为已完成了)。

启动 NSOperation 有两种方式

  1. NSOperation 可以配合 NSOperationQueue使用;

    只需将 NSOperation 添加到 NSOperationQueue

    系统会从 NSOperationQueue 中获取 NSOperation 然后添加到一个新线程中执行,这种方式默认 异步执行

  2. NSOperation 也可以独立使用。

    使用 start 方法开启操作。

    这种方式默认 同步执行

    如果这个 NSOperation 还没有准备好(isReady 返回 NO),那么会触发异常。

推荐 NSOperation 配合 NSOperationQueue 一起使用。

操作依赖 dependencies

当需要以特定顺序执行 NSOperation 时,依赖 是一个方便的选择。

可以使用 addDependency:removeDepencency 来添加或移除依赖。默认情况下,如果一个 NSOperation 的依赖没有执行完成,那么它绝不会准备好;一旦它的最后一个执行完成,这个 NSOperation 就准备好了。

NSOperation 的依赖规则不会区分依赖操作是否真正完成(被取消也被认为完成)。不过,开发者可以决定当依赖操作被取消或未真正完成时是否继续完成这个 NSOperation

完成回调 completionBlock

NSOperation 完成之后,会在执行这个 NSOperation 的线程回调这个 completionBlock

不过,这里的完成,是真正的完成,cancel 是无法触发的。

completionBlock 是一个属性,通过 setter 直接设置即可。

符合 KVO 的属性

NSOperation 类的部分属性是符合 KVC 和 KVO 的。

  • isCancelled

    是否被 cancel。只读。

  • isAsynchronous

    是否是异步执行的。只读。

  • isExecuting

    是否正在执行。只读。

  • isFinished

    是否已完成。只读。

  • isReady

    是否已准备好被执行。只读。

  • dependencies

    所有的依赖项。只读。

  • queuePriority

    队列优先级。可读可写。

  • completionBlock

    完成回调 Block。可读可写。

在子类化 NSOperation 时,如果对上述几个属性提供了自定义实现,务必实现 KVC 和 KVO。同样的,要是新增了一些属性,最好也实现 KVC 与 KVO。

子类 NSBlockOperation

NSBlockOperation 以 Block 形式存储任务,使用非常简单。

  • 创建 NSBlockOperation
/// 创建方式一:类方法 blockOperationWithBlock:
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"我是 NSBlockOperation 的任务");
}];

/// 创建方式二:
NSBlockOperation *blockOperation2 = [[NSBlockOperation alloc] init];

一般都是使用第一种方式。毕竟第一种方式直接创建了实例并加入了任务。

  • NSBlockOperation 添加任务

如果执行以下 blockOperation2 ,你会发现没有任何反应。这也是合乎情理的,毕竟里边没有任何任务。

我们可以使用 addExecutionBlock: 方法来为 NSBlockOperation 添加任务。

[blockOperation2 addExecutionBlock:^{
    NSLog(@"我是 NSBlockOperation 的任务");
}];

可以使用 addExecutionBlock: 来为一个 NSBlockOperation 实例添加任务。不论是通过 addExecutionBlock 添加的任务,还是 blockOperationWithBlock 初始化时传入的任务,都保存在其实例属性 executionBlocks 中。没错,一个 NSBlockOperation 实例可以存在多个任务。

可以看出:直接在当前线程执行,并且会阻塞当前线程。

子类 NSInvocationOperation

创建 NSInvocationOperation 的方式也有两种:

/// 创建方式一:分别传入 target / selector / object 
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(theInvocationSelector) object:nil];

、

/// 创建方式二:直接传入 NSInvocation 实例
//    Method theMethod = class_getInstanceMethod([self class], @selector(theInvocationSelector));
//    const char *theType = method_getTypeEncoding(theMethod);
//    NSMethodSignature *theMethodSignature = [NSMethodSignaturesignatureWithObjCTypes:theType];

NSMethodSignature *theMethodSignature = [self methodSignatureForSelector:@selector(theInvocationSelector)];

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:theMethodSignature];

NSInvocationOperation *invicationOperation2 = [[NSInvocationOperation alloc]
initWithInvocation:invocation];

第一种方式较好理解。第二种方式要传入 NSInvocation 实例,不知道是什么东西的朋友可以将 NSInvocation 理解为可以传多个参数的 performSelector:withObject: 即可(或者看一下 iOS - NSInvocation的使用 和 改进的 performSelector)。

自定义子类

自定义 NSOperation 的子类有蛮多需要注意的点。

先借用 大佬的图 来看一下 NSOperation 几个重要方法的默认实现:

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第19张图片

另外,NSOperation 有一个非常重要的概念:状态。这些状态改变时,需要发出 KVO 通知,也用一下 大佬的图:

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第20张图片

如果只需要自定义非异步(也就是同步)的 NSOperation ,只需要重写 main 方法就好了。如果还想要重写访问 NSOperation 数据的 gettersetter ,那请一定保证这些方法时线程安全的。

默认情况下, main 方法不做任何事情。在重写该方法时,不要调用 [super main]

同时,main 方法将自动在一个自动释放池中执行,无所另外创建自动释放池。

但对于异步的 NSOperation, 那么至少要重写以下方法或属性:

  1. start

    以异步的方式启动操作。

    一旦启动,更新操作的执行属性 executing

    start 前,必须检查是否已经被 cancel。若已被取消

    绝对不能调用 [super start]

  2. asynchronous

    返回 YES 即可,最好实现 KVO 通知。

  3. executing

    线程安全地返回操作的执行状态。

    值发生变化时必须发出 KVO 通知。KVO 的 keyPath 为 isExecuting。

  4. finished

    线程安全地返回操作的完成状态。

    值发生变化时必须发出 KVO 通知。

    一旦操作被取消,任务也被认为完成了。(操作队列在任务完成之后才会移除该操作)

当然,重写以上属性只是最低要求,实际开发中,我们肯定需要重写更多。

从 Apple 官方文档中的 Maintaining Operation Object States 可以找到各个 KVO 支持的属性的 keyPath:

属性 KVO 的 keyPath 备注
ready isReady 一般情况无需重写此属性。但如果 ready 的值由外部因素决定,开发者最好提供自定义实现。取消一个正在等待依赖项完成的 NSOperation,这些依赖项将被忽略而直接将此属性的值更新为 YES,以表示可正常运行。此时,操作队列将更快将其移除。
executing isExecuting 若重写 start 方法,则必须重写该属性。并在其值改变时发出 KVO 通知
finished isFinished 若重写 start 方法,则必须重写该属性,并在 NSOperation 完成执行或被取消时将值置为 YES 并发出 KVO 通知
cancelled isCancelled 不推荐发出此属性的 KVO 通知,毕竟 cancel 时该 NSOperation 的属性 readyfinished 的值也会更改

注意

  1. 自定义 NSOperation 时,务必支持 cancel 操作。

    执行任务的主流程应该周期性地检查 cancelled 属性。如果返回 YES,NSOperation 应该尽快清理并退出。

    如果重写了 start 方法,那么就必须包括取消操作的早期检查。

  2. 自行管理属性 executingfinished时, 务必在 executing 属性值置回 NO 时将 finished 属性值置为 YES。

    即使在开始执行之前被取消,也一定要处理好这些改变。

学学大佬 AFNetworking

当然,这里看的并不是最新版本,而是 AFNetworking 的 2.3.1 版本。

AFNetworking 3.0 之后的版本全面使用 NSURLSessionNSURLSession 本身异步、且不需要 runloop 的配合。因此 3.0 之后的版本并没有使用 NSOperation

AFURLConnectionOperation 是个 异步NSOperation 子类。来看一看:

  • 启动操作 -start
- (void)start {
    /// # 1
    [self.lock lock];

    /// # 2
    if ([self isCancelled]) {
        /// # 2.1
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } 
    /// # 3
    else if ([self isReady]) {
        /// # 3.1
        self.state = AFOperationExecutingState;

        /// # 3.2
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }

    /// # 4
    [self.lock unlock];
}

  1. 使用 NSRecursiveLock(递归锁)加锁,保证线程安全;

  2. 检查 NSOperation 是否已被 cancel;

    2.1 通过 子线程 执行取消操作。

  3. 检查 NSOperation 是否已经 ready;

    3.1 更新状态 state,并发出 KVO 通知。其内部也使用了递归锁;

    [图片上传中...(image-3a66e5-1592458065424-0)]

    3.2 通过 子线程 开始网络请求。

  4. 操作完成之后,将 NSRecursiveLock(递归锁)解锁。

start 中可以看出, AFNetworking 通过子线程来执行取消操作与真正的任务,来看一看:

  • 专用子线程 +networkRequestThread
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    /// # 2.1
    @autoreleasepool {
        /// # 2.2
        [[NSThread currentThread] setName:@"AFNetworking"];

        /// # 2.3
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        /// # 2.4
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    /// # 1
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        /// # 2
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

  1. 使用 dispatch_once 来创建子线程;

  2. 指定线程入口为 networkRequestThreadEntryPoint

    2.1 在 autoreleasepool 在执行操作,方便管理;

    2.2 更改线程名,方便使用;

    2.3 重点 创建 NSRunloop 对象,保持线程活跃,同时配合 NSURLConnection 执行网络请求;

    2.4 重点 创建 NSMechPort 对象,实现线程间通信,保证始终在创建的子线程处理逻辑。

看完子线程,再看看一下取消 connection 这个操作(这并不是 -cancel 方法):

  • 取消 connection cancelConnection
- (void)cancelConnection {
    /// 收集错误信息,就不放了
    /// ...

    /// # 1
    if (![self isFinished]) {
        /// # 2
        if (self.connection) {
            /// # 2.1
            [self.connection cancel];
            /// # 2.2
            [self performSelector:@selector(connection:didFailWithError:) withObject:self.connection withObject:error];
        } 
        /// # 3
        else {
            self.error = error;
            [self finish];
        }
    }
}

  1. 检查是否已经 finished;

  2. 检查 connection 属性是否存在,若存在;

    2.1 取消当前网络请求;

    2.2 调用 onnection:didFailWithError: 保存错误信息,在内部清理工作并执行终结操作 finish

  3. connection 不存在。

    保存错误信息,然后直接执行终结操作 finish

  • 终结操作 -finish
- (void)finish {
    [self.lock lock];
    self.state = AFOperationFinishedState;
    [self.lock unlock];

    /// ...
}

简单明了,在保证线程安全的同时,利用 -setState: 管理自身状态并发出 KVO 通知。

  • 执行任务 -operationDidStart
- (void)operationDidStart {
    /// # 1
    [self.lock lock];
    /// # 2
    if (![self isCancelled]) {
        /// ...
    }
    [self.lock unlock];
}

  1. 这里依然使用 NSRecursiveLock(递归锁)保证线程安全;
  2. 再次检查是否已被 cancel(完全遵循 Apple 明确的 “定期检查 cancelled 属性。”);

好的,最后看一下 Apple 心心念念的取消操作

  • 取消操作 -cancel
- (void)cancel {
    /// # 1
    [self.lock lock];
    /// # 2
    if (![self isFinished] && ![self isCancelled]) {
        /// # 2.1
        [super cancel];

        /// # 2.2
        if ([self isExecuting]) {
            [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        }
    }
    [self.lock unlock];
}

  1. 依然使用 NSRecursiveLock(递归锁)保证线程安全;

  2. 检查自身状态,如果已经 finishedcancelled,那也除了解锁也没啥号执行的了。

    2.1 调用 [super cancel];(mainstart 务必不要调用父类方法)

    2.2 检查是否正在 executing。如果正在执行,跟 -start 中一样取消 connection 的任务。

可以看到, NSOperation 本身是存在 -cancel 方法的。但是这里还需要处理自身任务的 cancelConnection

毕竟这是个 NSOperation ,其状态不止取决于业务逻辑,还要与其父类沟通好,于是 AFNetworking 方法重写 readyexecutingfinished 的 getter。

- (BOOL)isReady {
    return self.state == AFOperationReadyState && [super isReady];
}

- (BOOL)isExecuting {
    return self.state == AFOperationExecutingState;
}

- (BOOL)isFinished {
    return self.state == AFOperationFinishedState;
}


面试资料:

面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431
面试题资料或者相关学习资料都在群文件中 进群即可下载!

最后,抄两个高端操作

  1. qualityOfService

    服务质量。表示 NSOperation 在获取系统资源时的优先级,默认为 NSQualityOfServiceDefault

    优先级最高为 NSQualityOfServiceUserInteractive

  2. queuePriority

    队列优先级。表示 NSOperation 在操作队列中的相对优先级,默认为 NSOperationQueuePriorityNormal

    统一操作队列中,优先级更高的 NSOperation 将会被先执行,当然前提是 ready 为 YES。

    最高优先级为 NSOperationQueuePriorityVeryHigh。吐槽,这个起名有点上头。。。

3.2 NSOperationQueue

NSOperationQueue ,基于优先级与就绪状态执行 NSOperation 的操作队列。

一旦一个 NSOpertaion 被加入到 NSOperationQueue 中,无法直接移除,除非它报告自己完成了操作,否则一直在操作队列中。

将一个 NSOperation 实例加入到 NSOperationQueue 之后,它的 asynchronous 已经没有任何作用了。此时,NSOperationQueue 只是调用 GCD 来异步执行它。

对于操作队列中 ready 为 YES 的 NSOperation,操作队列将选择 queuePriority 最大的执行。

  • 创建操作队列

NSOperationQueue 一共有两种:主队列、自定义队列。

/// 主队列(其实不能叫创建)
NSOperationQueue *theMainOperationQueue = [NSOperationQueue mainQueue];

/// 自定义队列

所有添加到主队列的 NSOperation 都会放到主线程执行。

添加到自定义队列的 NSOperation,默认放到子线程并发执行。

  • NSOperationQueue 添加操作

存在多种方法可以向 NSOperationQueue 添加操作。

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation1");
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation2");
}];

NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation3");
}];

NSBlockOperation *operation4 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation4");
}];

NSBlockOperation *operation5 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"operation5");
}];

/// 添加单个 NSOperation
[theOperationQueue addOperation:operation1];

/// 添加多个 NSOperation
[theOperationQueue addOperations:@[operation2, operation3, operation4, operation5] waitUntilFinished:NO];

/// 便利方法,直接添加 Block 到操作队列中
[theOperationQueue addOperationWithBlock:^{
    NSLog(@"addOperationWithBlock");
}];

对于 -addOperation:-addOperations:waitUntilFinished: 而言,一个 NSOperation 一次最多只能在一个操作队列中。如果该 NSOperation 已经在某个队列中,则 此方法将会抛出 NSInvalidArgumentException ;同样的,如果某个 NSOperation 正在被执行,也将抛出这个异常。而且,就算第二次添加使用的是同一个队列,也是会抛出该异常的。

需要提醒的是,-addOperations:waitUntilFinished: 的第二个参数如果传入 YES,那么将阻塞当前线程,直到第一个参数中的 NSOperation 全部 finished

最后来个特例:-addBarrierBlock:。意为添加栅栏方法,具体功效请查看方法 dispatch_barrier_asyn

  • NSOperationQueue 控制并发数量

NSOperationQueue 有一个名为 maxConcurrentOperationCount 的属性。这个属性的值用来控制一个操作队列中同时最多可以有多少个 NSOperation 参与并发执行。

maxConcurrentOperationCount 默认为 -1,即不限制。同时 Apple 也推荐我们设置为该值,这个值会使系统根据系统条件来设置最大的值。

  • 暂停/恢复操作 suspended

suspended 其实只是一个属性。当我们设置它为 YES 时,此时就将队列暂停了;同时将其设置为 NO 时,此时队列恢复。

这里所谓的暂停,并不是设置之后立马暂停,而是执行当前正在执行的操作之后不继续执行。

  • 取消操作 -cancelAllOperations

调用 -cancelAllOperations 可以直接取消队列中的所有操作。就是所有操作。。。

suspended 不同,suspended 暂停之后可以恢复。而这里取消了就是真的取消了。

  • 操作同步 waitUntilAllOperationsAreFinished

调用此方法之后,阻塞当前线程,直到队列中的所有任务完成。


3.3 对比 GCD 与 NSOperationQueue

最后,借用 大佬的一张图 来对比一下 GCD 与 NSOperationQueue

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第21张图片

简单的任务使用 GCD 就好了。

如果需要控制并发数、取消任务、添加依赖关系等,那就使用 NSOperation Queue 好了。只不过很多时候都需要子类化 NSOperation。。。

四、iOS 中的锁

iOS 中有很多种锁,先摆上 ibireme 大佬在 不再安全的 OSSpinLock 的性能测试图 :

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第22张图片

当然,加锁方案是很多的,比如利用 串行队列栅栏方法调度组 也可以实现加锁目的。但是这里只讨论 真正的锁

先来了解几个概念 (参考子 维基百科):

  • 临界区:一块对公共资源进行访问的代码。就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。

  • 自旋锁:线程反复检查锁变量是否可用。在这个过程中,线程一直保持运行,所以是一种 忙碌等待。自旋锁有效避免了进程上下文的调度开销,这对于线程阻塞时间很短的场合很有效。但是,单核单线程 CPU 不适用于自旋锁。

  • 互斥锁:防止两条线程同时对同一公共资源(比如全局变量,这个变量就是互斥量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。等待互斥锁的线程进入休眠,被唤醒时需要进行上下文切换。

  • 读写锁:又称为 “共享-互斥锁” 与 “多读者-单写者锁”。用于解决多线程对公共资源的读写问题。读操作可并发重入,写操作是互斥的。读写锁通常用互斥锁、条件变量、信号量实现。

  • 条件锁:条件变量。任务需要的某些资源要求不满足时就进入休眠,条件锁锁上。当被分配到资源时,条件锁解开,任务程继续进行。条件变量总是与互斥量一同出现,

  • 递归锁:递归锁可以被一个线程多次 lock,且不会造成死锁问题。递归锁会追踪它被 lock 多少次,只有 unlock 次数与 lock 次数平衡才能真正释放这个锁。递归锁适用于递归方法。

  • 信号量:一个同步对象,用于保持在 0 至指定最大值之间的一个计数值。线程对该信号量完成一次 wait,计数值 -1;线程对该信号量完成一次 signal(release),计数值 +1。当计数值为 0 或小于 0 时,线程必须等待该信号量知道其数值大于 0。信号量适用于一个仅能同时被有限数量的用户使用的共享资源,是一种无需 忙碌等待 的锁。

先来一个抢火车票的经典场景:


- (void)trainTicket {

    self.trainTicketRemainder = 10000;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    dispatch_async(queue, ^{

        for (int i = 0; i < 1000; ++i) {

            [self buyTrainTicket];
            sleep(0.1);
        }
    });

    dispatch_async(queue, ^{

        for (int i = 0; i < 1000; ++i) {

            [self buyTrainTicket];
            sleep(0.2);
        }
    });

}

- (void)buyTrainTicket {
    if (self.trainTicketRemainder < 1) {
        NSLog(@"票量不足...");
        return;
    }

    self.trainTicketRemainder--;

    NSLog(@"售票成功,当前余量: %d", self.trainTicketRemainder);

}

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第23张图片

按照常理来讲,最后一个打印的 log 中的数量应该是 10000 - 1000 * 2 = 8000 才对。但是这里的数字是 8028。很明显,这违背了程序的正确性。

发生这种问题的主要原因就在于两个不同的线程同时在对一个共享资源(self.trainTicketRemainder)进行修改。为了避免这种问题,我们就需要对线程进行 加锁

加锁的原理也不难,来一段伪代码:

do {
    Acquire lock            /// 获得锁
        Critical section    /// 临界区     
    Release lock            /// 释放锁
        Reminder section    /// 非临界区
}


对于上述例子,可以将 self.trainTicketRemainder-- 这句代码作为临界区。

4.1 OSSpinLock 自旋锁

从上边的图可以看出,这种锁性能最佳,但是它已经不安全了。

简单描述一下这里的不安全:低优先级线程拿到锁时,高优先级会处于 OSSpinLock 的忙等待状态而消耗大量 CPU 时间,这使低优先级线程抢不到 CPU 时间,从而导致低优先级线程无法完成任务并释放锁。更多请移步 不再安全的 OSSpinLock

这种问题被称为 优先级反转。(这里建议先看一下线程服务质量 qos_class,或者看一下 2.1 创建队列 中全局队列出的的线程优先级)

使用 OSSpinLock 需要 #import

/// 初始化 OSSpinLock
/// OS_SPINLOCK_INIT 默认值为 0,在 locked 状态下大于 0,unlocked 状态下也为 0。
OSSpinLock theOSSpinLock = OS_SPINLOCK_INIT;

/// @abstract 上锁
/// @param __lock : OSSpinLock 的地址
OSSpinLockLock(&theOSSpinLock);

/// @abstract 解锁
/// @param __lock : OSSpinLock 的地址
OSSpinLockUnlock(&theOSSpinLock);

/// @abstract 上锁
/// @discussion 尝试加锁,可以加锁理解加锁并返回 YES,否则返回 NO
/// @param __lock :OSSpinLock 的地址
OSSpinLockTry(&theOSSpinLock);

注意 OSSpinLock 自 iOS 10 已被废弃,使用 os_unfair_lock 代替。

列个表:

种类 备注
OSSpinLock 自旋锁 不安全,iOS 10 已启用
os_unfair_lock 互斥锁 替代 OSSpinLock
pthread_mutex 互斥锁 PTHREAD_MUTEX_NORMAL#import
pthread_mutex (recursive) 递归锁 PTHREAD_MUTEX_RECURSIVE#import
pthread_mutex (cond) 条件锁 pthread_cond_t#import
pthread_rwlock 读写锁 读操作重入,写操作互斥
@synchronized 互斥锁 性能差,且无法锁住内存地址更改的对象
NSLock 互斥锁 封装 pthread_mutex
NSRecursiveLock 递归锁 封装 pthread_mutex (recursive)
NSCondition 条件锁 封装 pthread_mutex (cond)
NSConditionLock 条件锁 可以指定具体条件值

4.2 os_unfair_lock 互斥锁

os_unfair_lock 时 Apple 推荐用于取代不安全的 OSSpinLock,但仅限于 iOS 10 及以上系统。

os_unfair_lock 是一种互斥锁,处于等待的线程不会像自旋锁那样忙等,而是休眠。

使用 os_unfair_lock 需要 #import

/// 初始化 os_unfair_lock
os_unfair_lock theOs_unfair_lock = OS_UNFAIR_LOCK_INIT;

/// @abstract 上锁
/// @param lock : os_unfair_lock 的地址
os_unfair_lock_lock(&theOs_unfair_lock);

/// @abstract 解锁
/// @param lock : os_unfair_lock 的地址
os_unfair_lock_unlock(&theOs_unfair_lock);

/// @abstract 上锁
/// @discussion 尝试加锁,可以加锁理解加锁并返回 YES,否则返回 NO
/// @param lock : os_unfair_lock 的地址
os_unfair_lock_trylock(&theOs_unfair_lock);

4.3 pthread_mutex 互斥锁

pthread 表示 POSIX thread,定义了一组跨平台的线程相关的 API。

pthread_mutex 可以是一个互斥锁。

使用 pthread_mutex 需要 #import

/// 定义一个属性变量
pthread_mutexattr_t attr;

/// @abstract 初始化属性
/// @param attr : 属性的地址
pthread_mutexattr_init(&attr);

/// @abstract 设置属性类型为 PTHREAD_MUTEX_NORMAL
/// @param __lock : 属性 pthread_mutexattr_t 的地址
/// @param type : 锁的类型
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);

/// 定义一个锁变量
pthread_mutex_t mutex;

/// @abstract 使用指定属性初始化锁
/// @param mutex : 锁的地址
/// @param attr : 属性 pthread_mutexattr_t 的地址
pthread_mutex_(&mutex, &attr);

/// @abstract 销毁属性
/// @param attr : 属性的地址
pthread_mutexattr_destroy(&attr);

/// @abstract 上锁
/// @param mutex : 锁的地址
pthread_mutex_lock(&mutex);

/// @abstract 解锁
/// @param mutex : 锁的地址
pthread_mutex_unlock(&mutex);

/// @abstract 销毁 pthread_mutex
/// @discussion 一般在 dealloc 中执行
/// @param mutex : 锁的地址
pthread_mutex_destroy(&mutex);

其中,在 pthread_mutexattr_settype 方法的第二个参数代表锁的类型,一共有四种:

#define PTHREAD_MUTEX_NORMAL        0                       /// 普通的锁
#define PTHREAD_MUTEX_ERRORCHECK    1                       /// 错误检查
#define PTHREAD_MUTEX_RECURSIVE     2                       /// 递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL    /// 默认的锁,也就是 PTHREAD_MUTEX_NORMAL

当类型是 PTHREAD_MUTEX_DEFAULT 时,相当于 null。所以上边可以改写为:

pthread_mutexattr_settype(&attr, null);

4.4 pthread_mutex ( recursive ) 递归锁

在上一节中,说到 pthread_mutexattr_settype 方法的第二个参数有多种取值。如果这个值传入 PTHREAD_MUTEX_RECURSIVE,由设置此值属性初始化的 pthread_mutex 就是一个递归锁。

如果是互斥锁或者互斥锁,一个线程对同一个锁加锁多次,那么定会造成思索。但是递归锁允许一个线程对同一个锁多次加锁,不会造成死锁问题。不过,只有 unlock 次数与 lock 次数平衡时,递归锁才会真正释放。

使用 pthread_mutex 需要 #import

相关方法演示这里就不贴了,跟上一节几乎一模一样,除了这一句 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)

不过举一个例子:

- (void)display_PTHREAD_MUTEX_RECURSIVE {

    /// 定义一个属性
    pthread_mutexattr_t attr;
    /// 初始化属性
    pthread_mutexattr_init(&attr);
    /// 设置锁的类型
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

    /// 初始化锁
    pthread_mutex_init(&mutex, &attr);

    /// 销毁属性
    pthread_mutexattr_destroy(&attr);

    [self test_PTHREAD_MUTEX_RECURSIVE];

    /// 销毁锁(一般在 dealloc 中)
    pthread_mutex_destroy(&mutex);

}

- (void)test_PTHREAD_MUTEX_RECURSIVE {

    static int count = 5;

    // 第一次进来直接加锁,第二次进来,已经加锁了。还能递归继续加锁
    pthread_mutex_lock(&mutex);
    NSLog(@"加锁 %@", [NSThread currentThread]);

    if (count > 0) {
        count--;
        [self test_PTHREAD_MUTEX_RECURSIVE];
    }

    NSLog(@"解锁 %@", [NSThread currentThread]);
    pthread_mutex_unlock(&mutex);

}

阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第24张图片

4.5 pthread_mutex 条件锁

pthread_mutex 除了互斥锁、递归锁,还可以扮演条件锁。

不过, pthread_mutex 想要扮演条件锁,还需要条件变量 pthread_cond_t 的配合。

使用 pthread_mutex 需要 #import

/// 初始化锁,使用默认属性
pthread_mutex_init(&mutex, NULL);

/// 定义一个条件变量
pthread_cond_t cond;

/// 初始化条件变量
pthread_cond_init(&cond, NULL);

/// 等待条件(进入休眠时,放开 mutex 锁;被唤醒后,对 mutex 重新加锁)
pthread_cond_wait(&cond, &mutex);

/// 唤醒一个正在等待该条件的线程
pthread_cond_signal(&cond);

/// 唤醒所有正在等待该条件的线程
pthread_cond_broadcast(&cond);

/// 销毁条件变量
pthread_cond_destroy(&cond);

/// 销毁 mutex 锁
pthread_mutex_destroy(&mutex);


条件锁的使用场景并不是特别多。这里使用 “生产者 - 消费者”来演示一下。

先定义几个变量:

/// mutex 锁
pthread_mutex_t mutex;
/// 条件变量
pthread_cond_t cond;
/// 用于保存数据
NSMutableArray      *shop;

上代码:

- (void)setup_pthread_cond {

    /// 初始化锁,使用默认属性
    pthread_mutex_init(&mutex, NULL);

    /// 初始化条件变量
    pthread_cond_init(&cond, NULL);

    /// 唤醒所有正在等待该条件的线程
    pthread_cond_broadcast(&cond);

    shop = [NSMutableArray array];

    NSLog(@"请开始你的表演...");

    dispatch_queue_t theQueue = dispatch_get_global_queue(0, 0);
    dispatch_async(theQueue, ^{
        [self produce];
    });

    dispatch_async(theQueue, ^{
        [self buy];
    });

}

/// 假装是生产者
- (void)produce {

    while (true) {

        pthread_mutex_lock(&mutex);

        /// 生产需要时间(doge)
        sleep(0.1);

        if (shop.count > 5) {
            NSLog(@"商店满了,不能再生产了");
            pthread_cond_wait(&cond, &mutex);
        }

        /// 将生产的产品丢进商店
        [shop addObject:@"fan"];
        NSLog(@"生产了一个 fan");

        /// 唤醒一个正在等待的线程
        pthread_cond_signal(&cond);

        pthread_mutex_unlock(&mutex);
    }
}

/// 假装是消费者
- (void)buy {

    while (true) {

        pthread_mutex_lock(&mutex);

        /// shop 内没有存货,买不到
        /// 进入等待(进入休眠,放开 _mutex;被唤醒时,会重新对 _mutex 加锁)
        if (shop.count < 1) {
            NSLog(@"现在买不到, 我等一下吧");
            pthread_cond_wait(&cond, &mutex);
        }

        [shop removeObjectAtIndex:0];
        NSLog(@"终于买到了,不容易");

        pthread_cond_signal(&cond);

        pthread_mutex_unlock(&mutex);
    }
}


阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇)_第25张图片

iOS设计模式之(二)生产者-消费者 提出了使用 条件锁 的场景。

不得不说,生产者 - 消费者 这种模式能很好地解决 【夺命连环 call】。

4.6 pthread_rwlock 读写锁

pthread_rwlock,对鞋所。又称为 “共享-互斥锁” 与 “多读者-单写者锁”。用于解决多线程对公共资源的读写问题。读操作可并发重入,写操作是互斥的。

使用 pthread_rwlock 需要 #import

pthread_rwlock_t rwlock;

/// 初始化锁
pthread_rwlock_init(&rwlock, NULL);

/// 读 - 加锁
pthread_rwlock_rdlock(&rwlock);
/// 读 - 尝试加锁
pthread_rwlock_tryrdlock(&rwlock);

/// 写 - 加锁
pthread_rwlock_wrlock(&rwlock);
/// 写 - 尝试加锁
pthread_rwlock_trywrlock(&rwlock);

/// 解锁
pthread_rwlock_unlock(&rwlock);

/// 销毁锁
pthread_rwlock_destroy(&rwlock);

代码演示:

- (void)setup_pthread_rwlock {

    // pthread_rwlock_t rwlock;

    /// 初始化锁
    pthread_rwlock_init(&rwlock, NULL);

    dispatch_queue_t theQueue = dispatch_get_global_queue(0, 0);

    for (int i = 0; i < 3; ++i) {
        dispatch_async(theQueue, ^{
            [self write];
        });
    }

    for (int i = 0; i < 3; ++i) {
        dispatch_async(theQueue, ^{
            [self read];
        });
    }
}

- (void)write {
    pthread_rwlock_wrlock(&rwlock);

    sleep(3);
    NSLog(@"%s", __func__);

    pthread_rwlock_unlock(&rwlock);
}

- (void)read {
    pthread_rwlock_rdlock(&rwlock);

    sleep(1);
    NSLog(@"%s", __func__);

    pthread_rwlock_unlock(&rwlock);
}

4.7 @synchronized 互斥锁(递归锁)

@synchronized 是 iOS 中使用最简单的锁,但是也是性能最差的锁(见第四章开头的图)。

@synchronized 是互斥锁,当然他也是一个递归锁,不然怎么可能嵌套呢?

它需要一个参数,这个参数是我们要锁住的对象。如果不知道要锁住啥,那就选择 self。

简单的用法演示:

- (void)setup_synchronized {

    dispatch_queue_t theQueue = dispatch_get_global_queue(0, 0);

    for (int i = 0; i < 3; ++i) {
        dispatch_async(theQueue, ^{
            [self display_synchronized];
        });
    }

}

- (void)display_synchronized {

    @synchronized (self) {
        sleep(3);
        NSLog(@"%@", [NSThread currentThread]);
    }
}

注意 @synchronized 无法锁住“被加锁对象”地址更改的情况,具体原因看这里:

原理也很简单。在 objc_sync_enter 利用 id2data 将传入的对象 id 转换为 SyncData,然后利用 SyncData.mutex->lock()。Clang 将 @synchronized 改写的源码 clang - RewriteObjC.cpp,真正实现 objc_sync_enter 源码在 runtime 中,下载地址 Apple 官网、github 。

另外,建议大家看下这篇文章 关于 @synchronized,这儿比你想知道的还要多 。

4.8 NSLock 互斥锁

NSLock,互斥锁。由属性为 PTHREAD_MUTEX_NORMALpthread_mutex 封装而来的。

iOS 中存在一个 NSLocking 协议:

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

NSLock 遵循 NSLocking 协议,可以直接使用 - lock 来加锁,使用 - unlock 来解锁。

此外,NSLock 还提供了 - lockBeforeDate:- tryLock 两种便利性方法。

NSLock 用法演示:

/// 初始化一个 NSLock
NSlock *lock = [[NSLock alloc] init];

/// 加锁
[lock lock];

/// 解锁
[lock unlock];

/// 在 10s 内加锁,成功返回 YES,否则返回 NO。
[lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];

/// 尝试加锁,成功返回 YES,否则返回 NO。
[lock tryLock];

为了方便, NSLock 还提供一个名为 name 的属性。我们可以头通过这个属性来方便开发。

4.9 NSRecursiveLock 递归锁

NSRecursiveLock,递归锁。由属性为 PTHREAD_MUTEX_RECURSIVEpthread_mutex 封装而来的。

NSLock 相同,NSRecursiveLock 遵循了 NSLocking 协议可以直接使用 - lock 来加锁,使用 - unlock 来解锁。

其 API 与 NSLock 一致,使用方法也完全相同。

面试资料:

面试题持续整理更新中,如果你想一起进阶去大厂,不妨添加一下交流群1012951431

面试题资料或者相关学习资料都在群文件中 进群即可下载!


你可能感兴趣的:(阿里、字节:一套高效的iOS面试题( 多线程 GCD底层原理篇))