iOS面试题收纳-多线程

320 阅读38分钟

进程和线程区别

什么是进程?

计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配基本单位。

通俗的讲就是正在运行的程序,负责程序的内存分配,每一个进程都有自己独立的虚拟内存空间

什么是线程

线程是进程中一个独立执行的路径(控制单元),一个进程至少包含一条线程,即主线程。可以将耗时的执行路径(如网络请求)放在其他线程中执行

进程和线程的比较

  • 线程是 CPU 调度的最小单位
  • 进程是 系统 资源分配的最小单位
  • 一个程序可以对应多个进程,一个进程中可有多个线程,但至少要有一条线程
  • 同一个进程内的线程共享进程资源

多进程与多线程

多进程

  1. 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的(静态的),进程是活的(动态的)。

  2. 进程可以分为系统进程和用户进程。

    • 凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身;
    • 所有由用户启动的进程都是用户进程。
  3. 在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。

多线程

  1. 是指实现多个线程并发执行的技术,进而提升整体处理性能
  2. 同一时间CPU只能处理一条线程,多线程并发执行其实是 CPU 快速的在多条线程之间调度(切换)。如果 CPU 调度线程的时间足够快,就造成了多线程并发执行的假象
优势
  1. 充分发挥多核处理器的优势,将不同线程任务分配给不同的处理器,能适当提高程序的执行效率
  2. 能适当提高资源利用率(CPU、内存利用率)
弊端
  1. 创建线程是有开销的,iOS下主要成本包括:
    • 内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB) 也可以使用setStackSize设置,但必须是4K的倍数,而且最小为16K
    • 创建线程大约需要90毫秒的创建时间;
  2. 开启大量的线程,CPU会在N多线程之间调度,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)
  3. 线程越多,CPU在调度线程上的开销越大,程序设计更加复杂:比如线程之间的通信、多线程的数据共享

多进程间怎么通信

IPC的常见方式

  1. 进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
  2. IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC
    • 管道:速度慢,容量有限,只有 父子进程 能通讯
    • 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
    • 信号量:不能传递复杂消息,只能用来同步
    • 共享内存区:能够很容易控制容量,速度快,但要保持同步

iOS中进程间通信

  1. iOS系统是相对封闭的系统,App各自在各自的沙盒(sandbox)中运行
  2. 每个App都只能读取iPhone上系统为该应用程序程序创建的文件夹AppData下的内容,不能随意跨越自己的沙盒去访问别人App沙盒中的内容
URLcheme
  1. 这个是iOS APP通信最常用到的通信方式,APP1通过openURL的方法跳转到APP2,并且在URL中带上想要的参数
  2. 使用方法也很简单
    • 只需要源APP1在info.plist中配置LSApplicationQueriesSchemes,指定目标App2的scheme;
    • 然后在目标App2的info.plist 中配置好URLtypes,表示该App接受何种URL scheme的唤起。
keychain-钥匙串
  1. iOS 系统的keychain是一个安全的存储容器,它本质上就是一个sqlite数据库
  2. 它的位置存储在/private/var/Keychains/keychain-2.db
  3. 它的所有数据都是经过加密的,可以用来为不同的APP保存敏感信息,
  4. 它是独立于每个APP的沙盒之外的,即使APP被删除,keychain里面的信息依然存在。
UIPasteBoard-剪切板
UIDocumentInteractionController

主要是用来实现同设备上APP之间的贡献文档,以及文档预览、打印、发邮件和复制等功能。

UIActivityViewController

iOS SDK 中封装好的类,在APP之间发送数据、分享数据和操作数据。

Local socket
  1. 一个APP1在本地的端口port1234 进行TCP的bind 和 listen
  2. 另外一个APP2在同一个端口port1234发起TCP的connect连接,
  3. 建立正常的TCP连接,进行TCP通信了,然后想传什么数据就可以传什么数据
APP Groups

用于同一个开发团队开发的APP之间,包括APP和extension之间共享同一份读写空间,进行数据共享

Airdrop

通过 Airdrop实现不同设备的APP之间文档和数据的分享

多线程间怎么通信

一个线程传递数据给另一个线程

  1. NSThread 可以先将当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,
  2. 使用下面的方法进行线程间的通信,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法
- (void)performSelectorOnMainThread:(SEL)aSelector
  withObject:(nullable id)arg 
    waitUntilDone:(BOOL)wait;

- (void)performSelector:(SEL)aSelector
  onThread:(NSThread *)thr
    withObject:(nullable id)arg 
      waitUntilDone:(BOOL)wait

在一个线程中执行完特定的任务后,转到另一个线程继续执行任务

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
});

什么是线程安全

  1. 一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源。比如多个线程访问同一个对象、同一个变量、同一个文件

  2. 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

  3. 在并发执行的环境中

    • 对于共享数据,通过同步机制保证各个线程都可以正确的执行,不会出现数据污染的情况

    • 对于某个资源,在被多个线程访问时,不管运行时执行这些线程有什么样的顺序或者交错,不会出现错误的行为,就认为这个资源是线程安全的

    • 一般来说,对于某个资源如果只有读操作,则这个资源无需同步就是线程安全的,若有多个线程进行读写操作,则需要线程同步来保证线程安全

多线程同步方式

线程同步是两个或多个共享关键资源的线程的并发执行。同步的作用就是避免关键资源的使用冲突。

临界区(Critical section)

  1. 通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
  2. 在任何时候只允许一个线程访问共享资源,如果有多个线程访问,那么当有一个线程进入后,其他试图访问共享资源的线程将会被挂起,并且等到进入临界区的线程离开,临界被释放后,其他线程才可以抢占。

互斥量(Mutex)

  1. 为协调对一个共享资源的单独访问而设计
  2. 互斥量只有一个,只有拥有互斥量的线程,才有权限去访问系统的公共资源,保证资源不会被多个线程访问。
  3. 互斥不仅能实现同一个应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享

信号量(Semphore)

  1. 为控制一个具有有限数量的用户资源而设计。
  2. 它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。

事件(Event):

  1. 用来通知线程有一些事件已发生,从而启动后继任务的开始。

信号量和互斥量的区别

互斥量用于线程的互斥,信号量用于线程的同步

这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别

互斥
  1. 指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性
  2. 但互斥无法限制访问者对资源的访问顺序,即访问是无序
同步
  1. 指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问
  2. 在大多数情况下,同步已经实现了互斥,所有写入资源的情况必定是互斥的
  3. 少数情况是指可以允许多个访问者同时访问资源
互斥量值只能为0/1,信号量值可以为非负整数
  1. 一个互斥量只能用于对一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。
  2. 信号量可以实现多个同类资源的多线程互斥和同步。
  3. 当信号量为单值信号量时,也可以完成一个资源的互斥访问
互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到

什么是执行任务

  1. 就是执行操作的意思,也就是 在线程中执行的那段代码。在 GCD 中是放在 block 中的
  2. 执行任务有两种方式:同步执行(sync)和异步执行(async)

同步执行(Sync)

  1. 同步添加任务到指定的队列中,在添加的任务执行结束之前会一直等待,直到队列里面的任务完成之后再继续执行,即 会阻塞线程,在当前线程中执行任务
  2. 只能在当前线程中执行任务(是当前线程,不一定是主线程),不具备开启新线程的能力

异步执行(Async)

  1. 线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程
  2. 可以在新的线程中执行任务,具备开启新线程的能力(并不一定开启新线程)
  3. 如果不是添加到主队列上,异步会在新的线程中执行任务

什么是队列

  1. 这里的队列指执行任务的等待队列,即 用来存放任务的队列
  2. 队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。
  3. 每读取一个任务,则从队列中释放一个任务
  4. 在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。

串行队列(Serial Dispatch Queue)

  1. 同一时间内,队列中只能执行一个任务,只有 当前的任务执行完成之后,才能执行下一个任务。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  2. 按照FIFO顺序执行,对于每一个不同的串行队列,系统会为这个队列建立唯一的线程来执行代码
  3. 主队列是主线程上的一个串行队列,是系统自动为我们创建的。

并发队列(Concurrent Dispatch Queue)

  1. 同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)
  2. 并发队列的 并发功能只有在异步(dispatch_async)函数下才有效
  3. 可以让多个任务按照FIFO的顺序开始执行,注意是*开始*,但是它们的执行结束时间是不确定的,取决于每个任务的耗时。对于n个并发队列,GCD不会创建对应的n个线程而是进行适当的优化

组合队列执行表

『主线程』中,『不同队列』+『不同任务』

task_combination0.png

『不同队列』+『不同任务』 组合,以及 『队列中嵌套队列』

task_combination1.png

只要是同步函数或者是主队列,就不会开启线程,在当前线程串行执行

使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)

并行和并发的区别

  • 并行是指两个或者多个事件在同一时刻发生

  • 并发是指两个或多个事件在同一时间间隔内发生

iOS中多线程方案

pthread、NSThread、GCD、NSOperation

技术方案简介语言线程生命周期使用频率
pthread1. 一套通用的多线程API 2. 适用于Unix/Linux/Windows等系统 3. 跨平台、可移植 4. 使用难度大C程序员管理几乎不用
NSThread1. 使用更加面向对象 2. 简单易用,可直接操作线程对象OC程序员管理偶尔使用
GCD1. 旨在代替NSThread等线程技术 2. 充分利用设备的多核C自动管理经常使用
NSOperation1. 基于GCD(底层是GCD) 2. 比GCD多了一些简单使用的功能 3. 使用更加面向对象OC自动管理经常使用

NSThread:轻量级别的多线程技术

  1. 手动开辟的子线程,如果使用的是初始化方式就需要我们自己启动,如果使用的是构造器方式就会自动启动。
  2. 只要是我们手动开辟的线程,都需要我们自己管理该线程,不只是启动,还有该线程使用完毕后的资源回收
  NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"我是参数"];
    // 当使用初始化方法出来的主线程需要start启动
    [thread start];
    // 可以为开辟的子线程起名字
    thread.name = @"NSThread线程";
    // 调整Thread的权限 线程权限的范围值为0 ~ 1 。越大权限越高,先执行的概率就会越高,由于是概率,所以并不能很准确的的实现我们想要的执行顺序,默认值是0.5
    thread.threadPriority = 1;
    // 取消当前已经启动的线程
    [thread cancel];
    // 通过构造器开辟子线程,开辟后就开始执行任务
    [NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方式"];
performSelectorXXX
  1. 只要是NSObject的子类或者对象都可以通过调用方法进入子线程和主线程,其实这些方法所开辟的子线程也是NSThread的另一种体现方式。
  2. 在编译阶段并不会去检查方法是否有效存在,如果不存在只会给出警告
//在当前线程。延迟1s执行。响应了OC语言的动态性:延迟到运行时才绑定方法
[self performSelector:@selector(aaa) withObject:nil afterDelay:1];

// 回到主线程。waitUntilDone:是否将该回调方法执行完再执行后面的代码,
// 如果为YES:就必须等回调方法执行完成之后才能执行后面的代码,说白了就是阻塞当前的线程;
// 如果是NO:就是不等回调方法结束,不会阻塞当前线程
[self performSelectorOnMainThread:@selector(aaa) withObject:nil waitUntilDone:YES];

// 开辟子线程 
[self performSelectorInBackground:@selector(aaa) withObject:nil];

// 在指定线程执行
[self performSelector:@selector(aaa) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES]
需要注意的是
  1. 如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的Runloop中。
  2. 也就是如果当前线程没有开启runloop,该方法会失效。
  3. 在子线程中,需要启动runloop(注意调用顺序)
[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];
  1. performSelector:withObject:只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的Runloop中也能执行

什么是GCD

强烈建议阅读

GCD(Grand Central Dispatch),又叫做大中央调度,它对线程操作进行了封装,加入了很多新的特性,内部进行了效率优化,提供了简洁的C语言接口,使用更加高效,也是苹果推荐的使用方式

GCD中的队列类型

main queue

通过dispatch_get_main_queue获得,这是一个与主线程关联的串行队列

global queue
  1. 全局队列是并发队列,由整个进程共享。存在着高、中、低三种优先级的全局队列。
  2. 调用dispath_get_global_queue并传入优先级来访问队列
自定义队列

通过函数dispatch_queue_create创建的队列,可以创建串行队列也可以创建并发队列

如何去理解GCD执行原理?

  1. GCD有一个底层线程池,这个池中存放的是一个个的线程
  2. 这个池中的线程是可以重用的,当一段时间后线程没有被调用的话,这个线程就会被销毁
  3. 注意:开多少条线程是由底层线程池决定的(线程建议控制再3~5条),线程池是系统自动来维护,我们只需要向队列中添加任务,进行队列调度即可
  4. 如果队列中存放的是同步任务,则任务出队后,底层线程池会提供一条线程供这个任务执行,任务执行完毕后这条线程再回到线程池。这样队列中的任务反复调度,因为是同步的,所以当我们用currentThread打印的时候,就是同一条线程
  5. 如果队列中存放的是异步的任务(注意异步可以开线程),当任务出队后,底层线程池会提供一个线程供任务执行,因为是异步执行,队列中的任务不需等待当前任务执行完毕就可以调度下一个任务,这时底层线程池中会再次提供一个线程供第二个任务执行,执行完毕后再回到底层线程池中。这样就对线程完成了复用,而不需要每一个任务执行都开启新的线程,从而节约系统的开销,提高了效率。
GCD线程池
  • 不管是自定义队列、全局队列还是主队列最终都直接或者间接的依赖 12个root队列来执行任务调度
  • 尽管如此主队列有自己的label
  • 如果按照label计算总共16个,除了上面的12个
    • com.apple.main-thread
    • 两个内部管理队列com.apple.libdispatch-managercom.apple.root.libdispatch-manager
    • runloop的运行队列

image

有几个root队列? 12个
  • userInteractive、default、unspecified、userInitiated、utility 、background6个,他们的overcommit版本6个。
    • 支持overcommit的队列在创建队列时无论系统是否有足够的资源都会重新开一个线程。
    • 串行队列和主队列是overcommit的,创建队列会创建1个新的线程。
    • 并行队列是非overcommit的,不一定会新建线程,会从线程池中的64个线程中获取并使用。
  • 优先级 userInteractive > default > unspecified > userInitiated > utility > background
  • 全局队列是root队列,并行队列
  • 串行队列只有一个线程,线程num > 2
  • 不管优先级多高并行队列有最多有64个线程,线程num在3~66,在一次轮询中遇到高优先级的会先执行
  • 串行队列和并行队列都存在线程数多了1个,number最大到了67,不过串行队列的任务不一定在67这个线程中而是会复用前面的任意一个线程。说明串行队列加入时一定会创建一个线程
  • 当一个串行队列依附于一个并行队列时(非overcommit,如果是overcommit队列则会新建一个线程),线程最多恢复到了64个,并不会再新建一个线程了
有几个线程池?
  • 两个。一个是主线程池,另一个是除了主线程池之外的线程池
  • 主线程池由序列为1的主队列管理
一个队列最多支持几个线程同时工作? 64个
多个队列,允许最多几个线程同时工作?
  • 64个。优先级高的队列获得的可活跃线程数多于优先级低的,但也有例外,低优先级的也能获得少量活跃线程。
  • width=1意味着是串行队列,只有一个线程可用,width=0xffe则意味着并行队列,线程则是从线程池获取,可用线程数是64个
  • 对于 dispatch_asyn 的调用(同步操作线程都在主线程不再赘述)串行队列是overcommit的,创建队列会创建1个新的线程,并行队列是非overcommit的,不一定会新建线程,会从线程池中的64个线程中获取并使用
  • 一旦新建一个串行队列就会新建一个线程,避免在类似循环操作中新建串行队列,这个上限是多少是任意多吗?其实也不是最多新增512个(不算主线程,number从4开始到515)
  • 如果都分别创建了队列(overcommit)一般不会有影响,除非创建超过了512个,因为尽管是同一个root队列但是会创建不同的线程,此时当前root队列仅仅控制任务FIFO,但是并不是只有第一个任务执行完第二个任务才能开始,也就是说FIFO控制的是开始的节奏,但是任务在不同的thread执行不会阻塞。当然一个串行队列中的多个异步task是相互有执行顺序的

GCD相关API

dispatch_queue_create 源码

总结
  • 队列创建方法dispatch_queue_create中的参数二(即队列类型),决定了下层中 max & 1(用于区分是 串行 还是 并发),其中1表示串行
  • queue 也是一个对象,也需要底层通过alloc + init 创建,并且在alloc中也有一个class,这个class是通过宏定义拼接而成,并且同时会指定isa的指向
  • 创建队列在底层的处理是通过模板创建的,其类型是dispatch_introspection_queue_s结构体

dispatch_queue_create底层分析流程如下图所示

image.png

dispatch_sync 源码

  • 首先通过width判定是串行队列还是并发队列,如果是并发队列则调用_dispatch_sync_invoke_and_complete,串行队列则调用_dispatch_barrier_sync_f
  • 队列push以后就是用_dispatch_lock_is_locked_by判断将要调度的和当前等待的队列是不是同一个,如果相同则返回YES,产生死锁DISPATCH_CLIENT_CRASH
同步函数 + 并发队列 顺序执行的原因

_dispatch_sync_invoke_and_complete -> _dispatch_sync_function_invoke_inline源码中,主要有三个步骤:

  • 将任务压入队列: _dispatch_thread_frame_push
  • 执行任务的block回调: _dispatch_client_callout
  • 将任务出队:_dispatch_thread_frame_pop

从实现中可以看出,是先将任务push队列中,然后执行block回调,在将任务pop,所以任务是顺序执行的。

总结

同步函数的底层实现如下:

  • 同步函数的底层实现实际是同步栅栏函数
  • 同步函数中如果当前正在执行的队列和等待的是同一个队列,形成相互等待的局面,则会造成死锁

所以,综上所述,同步函数的底层实现流程如图所示

image.png

dispatch_async 源码

  • 全局队列使用_pthread_workqueue_addthreads开辟线程,对于其他队列使用pthread_create开辟新的线程
  • 用链表保存所有提交的 block(先进先出,在队列本身维护了一个链表,新加入block放到链表尾部)
  • 然后在底层线程池中,依次取出 block 并执行
_dispatch_continuation_init:任务包装函数
  • 通过_dispatch_Block_copy拷贝任务
  • 通过_dispatch_Block_invoke封装任务,其中_dispatch_Block_invoke是个宏定义,根据以上分析得知是异步回调
#define _dispatch_Block_invoke(bb) \
        ((dispatch_function_t)((struct Block_layout *)bb)->invoke)
  • 如果是同步的,则回调函数赋值为_dispatch_call_block_and_release
  • 通过_dispatch_continuation_init_f方法将回调函数赋值,即f就是func,将其保存在属性中
_dispatch_continuation_async:并发处理函数
总结
  • 【准备工作】:首先,将异步任务拷贝并封装,并设置回调函数func
  • 【block回调】:底层通过dx_push递归,会重定向到根队列,然后通过pthread_creat创建线程,最后通过dx_invoke执行block回调(注意dx_pushdx_invoke 是成对的)

异步函数的底层分析流程如图所示

image.png

dispatch_barrier_async 源码

  • dispatch_barrier_async源码和dispatch_async几乎一致,仅仅多了一个标记位DC_FLAG_BARRIER
  • 这个标记位用于在取出任务时进行判断,正常的异步调用会依次取出,而如果遇到了DC_FLAG_BARRIER则会返回,所以可以等待所有任务执行结束执行dx_push
  • (不过提醒一下dispatch_barrier_async必须在自定义队列才有用,原因是global队列没有v_table结构,同时不要试图在主队列调用,否则会crash)

dispatch_once 源码

总结

针对单例的底层实现,主要说明如下:

  • 【单例只执行一次的原理】:GCD单例中,有两个重要参数,onceTokenblock,其中onceToken是静态变量,具有唯一性,在底层被封装成了dispatch_once_gate_t类型的变量ll主要是用来获取底层原子封装性的关联,即变量v,通过v来查询任务的状态,如果此时v等于DLOCK_ONCE_DONE,说明任务已经处理过一次了,直接return
  • 【block调用时机】:如果此时任务没有执行过,则会在底层通过C++函数的比较,将任务进行加锁,即任务状态置为DLOCK_ONCE_UNLOCK,目的是为了保证当前任务执行的唯一性,防止在其他地方有多次定义。加锁之后进行block回调函数的执行,执行完成后,将当前任务解锁,将当前的任务状态置为DLOCK_ONCE_DONE,在下次进来时,就不会在执行,会直接返回
  • 【多线程影响】:如果在当前任务执行期间,有其他任务进来,会进入无限次等待,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的

单例的底层流程分析如下如所示

image.png

dispatch_source_t 源码

  • 无时间差则直接调用dispatch_async,否则先创建一个dispatch_source_t
  • 不同的是这里的类型并不是DISPATCH_SOURCE_TYPE_TIMER而是_dispatch_source_type_after,查看源码不难发现它只是dispatch_source_type_s类型的一个常量和_dispatch_source_type_timer并没有明显区别
  • 最终还是封装成一个dispatch_continuation_t进行同步或者异步调用,而上面_dispatch_after直接构建了dispatch_continuation_t进行执行

dispatch_group 源码

简单的说就是dispatch_group_asyncdispatch_group_notify本身就是和dispatch_group_enterdispatch_group_leave没有本质区别,后者相对更加灵活

总结
  • enter-leave只要成对就可以,不管远近
  • dispatch_group_enter在底层是通过C++函数,对group的value进行--操作(即0 -> -1)
  • dispatch_group_leave在底层是通过C++函数,对group的value进行++操作(即-1 -> 0)
  • dispatch_group_notify在底层主要是判断group的state是否等于0,当等于0时,就通知
  • block任务的唤醒,可以通过dispatch_group_leave,也可以通过dispatch_group_notify
  • dispatch_group_async 等同于enter - leave,其底层的实现就是enter-leave

所以综上所述,调度组的底层分析流程如下图所示

image.png

栅栏函数

GCD中常用的栅栏函数,主要有两种

  • 同步栅栏函数dispatch_barrier_sync(在主线程中执行):前面的任务执行完毕才会来到这里,但是同步栅栏函数会堵塞线程,影响后面的任务执行
  • 异步栅栏函数dispatch_barrier_async:前面的任务执行完毕才会来到这里。

栅栏函数最直接的作用就是 控制任务执行顺序,使同步执行

同时,栅栏函数需要注意一下几点

  • 栅栏函数控制同一并发队列
  • 同步栅栏添加进入队列的时候,当前线程会被锁死,直到同步栅栏之前的任务和同步栅栏任务本身执行完毕时,当前线程才会打开然后继续执行下一句代码。
  • 在使用栅栏函数时.使用自定义队列才有意义,如果用的是串行队列或者系统提供的全局并发队列,这个栅栏函数的作用等同于一个同步函数的作用,没有任何意义
总结
  • 异步栅栏函数阻塞的是队列,而且必须是自定义的并发队列,不影响主线程任务的执行
  • 同步栅栏函数阻塞的是线程,且是当前线程,会影响当前线程其他任务的执行

dispatch_after

  1. dispatch_after能让我们添加到队列中任务延时执行,该函数并不是在指定时间后执行任务,而是在指定时间追加任务到队列中
  2. 由于其内部使用的是dispatch_time_t而不是NSTimer。所以如果在子线程中调用,相比performSelector:afterDelay,不用关心runloop是否开启
//第一个参数是time,第二个参数是dispatch_queue,第三个参数是要执行的block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"dispatch_after");
});

dispatch_group_t

  1. 组调度可以实现等待一组操都作完成后,再执行后续任务
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{//请求1});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{//请求2});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //界面刷新
    NSLog(@"任务均完成,刷新界面");
});
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
    //do something
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_async(queue, ^{
    //do something
    dispatch_group_leave(group);
});

dispatch_group_notify(group, queue, ^{
    //do something
});

dispatch_semaphore

  • 用于控制最大并发数 信号量>1
dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
dispatch_semaphore_signal:发送一个信号,让信号总量加1
dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行
//crate的value表示,最多几个资源可访问
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);   
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//任务1
dispatch_async(quene, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"run task 1");
    sleep(1);
    NSLog(@"complete task 1");
    dispatch_semaphore_signal(semaphore);       
});
//任务2
dispatch_async(quene, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"run task 2");
    sleep(1);
    NSLog(@"complete task 2");
    dispatch_semaphore_signal(semaphore);       
});
//任务3
dispatch_async(quene, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"run task 3");
    sleep(1);
    NSLog(@"complete task 3");
    dispatch_semaphore_signal(semaphore);       
});
  • 保持线程同步,将异步执行任务转换为同步执行任务,信号量0
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSInteger number = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    number = 100;
	  dispatch_semaphore_signal解锁后继续执行
    dispatch_semaphore_signal(semaphore);
});

dispatch_semaphore_wait加锁阻塞了当前线程
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %zd",number);
  • 保证线程安全,为线程加锁,信号量1
在线程安全中可以将dispatch_semaphore_wait看作加锁,dispatch_semaphore_signal看作解锁

首先创建全局变量,注意到这里的初始化信号量是1
_semaphore = dispatch_semaphore_create(1);

- (void)asyncTask {
  dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
  count++;
  sleep(1);
  NSLog(@"执行任务:%zd",count);
  dispatch_semaphore_signal(_semaphore);
}

异步并发调用asyncTask
for (NSInteger i = 0; i < 100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self asyncTask];
    });
}

dispatch_barrier_(a)sync

  1. 一个dispatch barrier 允许在一个并发队列中创建一个同步点。
  2. 当在并发队列中遇到一个barrier,它会延迟执行barrier的block,等待所有在barrier之前提交的blocks执行结束。
  3. 这时barrier block自己开始执行。 之后, 队列继续正常的执行操作
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    // dosth1;
});
dispatch_async(queue, ^{
    // dosth2;
});
dispatch_barrier_async(queue, ^{
    // doBarrier;
});
dispatch_async(queue, ^{
    // dosth4;
});
dispatch_async(queue, ^{
    // dosth5;
});

dispatch_set_target_queue

- 第一个参数是要执行变更的队列(不能指定主队列和全局队列)
- 第二个参数是目标队列(指定全局队列)
dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t queue);

dispatch_set_target_queue 函数有两个作用:
第一,变更队列的执行优先级;
第二,目标队列可以成为原队列的执行阶层

dispatch_once

可以用disaptch_once来执行一次性的初始化代码,比如创建单例,这个方法是线程安全的。

+ (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    static id instance = nil;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}
死锁问题

用disaptch_once创建单例的时候,如果出现循环引用的情况,会造成死锁。比如A->B->C->A这种调用就会死锁。 可以参考滥用单例之dispatch_once死锁

GCD如何取消线程

dispatch_block_cancel可以取消还未执行的线程。没办法取消一个正在执行的线程

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_block_t block1 = dispatch_block_create(0, ^{
    NSLog(@"block1");
});
dispatch_block_t block2 = dispatch_block_create(0, ^{
    NSLog(@"block2");
});
dispatch_block_t block3 = dispatch_block_create(0, ^{
    NSLog(@"block3");
});
    
dispatch_async(queue, block1);
dispatch_async(queue, block2);
dispatch_async(queue, block3);
dispatch_block_cancel(block3); // 取消 block3

使用临时变量+return 方式取消 正在执行的Block

__block BOOL gcdFlag= NO; // 临时变量
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    for (long i=0; i<1000; i++) {
        NSLog(@"正在执行第i次:%ld",i);
        sleep(1);
        if (gcdFlag==YES) { // 判断并终止
            NSLog(@"终止");
            return ;
        }
    };
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"我要停止啦");gcdFlag = YES;});

说一下 OperationQueue 和 GCD 的区别,以及各自的优势

  • GCD是面向底层的C语言的API,NSOpertaionQueue用GCD构建封装的,是GCD的高级抽象
  • GCD执行效率更高,而且由于队列中执行的是由block构成的任务,写起来更方便
  • GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数、优先级、依赖关系等调整执行顺序
  • NSOperationQueue甚至可以跨队列设置依赖关系,GCD只能通过设置串行队列或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序。较为复杂
  • NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)
    • 实际项目开发中,很多时候只是会用到异步操作,不会有特别复杂的线程关系管理,GCD是首选
    • 如果考虑异步操作之间的事务性、顺序性、依赖关系,考虑使用NSOperationQueue

NSOperation和NSOperationQueue

操作(Operation)

  1. 执行操作的意思,换句话说就是你在线程中执行的那段代码。
  2. 在 GCD 中是放在 block 中的。在 NSOperation 中,使用 NSOperation 子类 NSInvocationOperation、NSBlockOperation或者自定义子类来封装操作。
NSOperation如何实现操作依赖

通过任务间添加依赖,可以为任务设置开始执行的先后顺序

NSOperationQueue *queue=[[NSOperationQueue alloc] init];
//创建操作
NSBlockOperation *operation1=[NSBlockOperation blockOperationWithBlock:^(){    NSLog(@"执行第1次操作,线程:%@",[NSThread currentThread]);
}];
NSBlockOperation *operation2=[NSBlockOperation blockOperationWithBlock:^(){    NSLog(@"执行第2次操作,线程:%@",[NSThread currentThread]);
}];
NSBlockOperation *operation3=[NSBlockOperation blockOperationWithBlock:^(){    NSLog(@"执行第3次操作,线程:%@",[NSThread currentThread]);
}];
//添加依赖
[operation1 addDependency:operation2];
[operation2 addDependency:operation3];
//将操作添加到队列中去
[queue addOperation:operation1];
[queue addOperation:operation2];
[queue addOperation:operation3];
NSOperation取消线程方式
通过 cancel 取消未执行的单个操作
NSOperationQueue *queue1 = [[NSOperationQueue alloc]init];
NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block11");
}];
NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block22");
}];
NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"block33");
}];
[block3 cancel];
[queue1 addOperations:@[block1,block2,block3] waitUntilFinished:YES];
移除队列里面所有的操作,但正在执行的操作无法移除
[queue1 cancelAllOperations];
挂起队列,使队列任务不再执行,但正在执行的操作无法挂起
queue1.suspended = YES;
我们可以自定义NSOperation,实现取消正在执行的操作。其实就是拦截main方法。
 main方法:
 1、任何操作在执行时,首先会调用start方法,start方法会更新操作的状态(过滤操作,如过滤掉处于“取消”状态的操作)。
 2、经start方法过滤后,只有正常可执行的操作,就会调用main方法。
 3、重写操作的入口方法(main),就可以在这个方法里面指定操作执行的任务。
 4main方法默认是在子线程异步执行的。

操作队列(Operation Queues)

  1. 这里的队列指操作队列,即用来存放操作的队列

  2. 不同于 GCD 中的调度队列 FIFO(先进先出)的原则。

    1. NSOperationQueue 对于添加到队列中的操作,首先会进入到准备就绪状态(就绪状态取决于操作之间的依赖关系)
    2. 进入就绪状态的操作 的开始执行顺序(非结束执行顺序)由操作之间相对的优先级决定(优先级是操作对象自身的属性)。
  3. 操作队列通过设置最大并发操作数(maxConcurrentOperationCount)来控制并发、串行。

  4. NSOperationQueue 为我们提供了两种不同类型的队列:主队列和自定义队列。

    主队列运行在主线程之上,而自定义队列在后台执行

多线程安全隐患的解决方案

官方源码:github.com/apple/swift…

GNUStep:www.gnustep.org/resources/d…

  1. 隐患造成原因:多个线程同时访问一个数据然后对数据进行操作

  2. 解决方案:使用线程同步技术

  3. 常见线程同步技术:加锁

有哪几种类型的锁?各自的原理?它们之间的区别是什么?最好可以结合使用场景来说

自旋锁
  1. 自旋锁在无法进行加锁时,会不断的进行尝试,处于忙等状态,一直占用CPU资源
  2. 一般用于临界区的执行时间较短的场景
互斥锁

对于某一资源同时只允许有一个访问,无论读写,平常使用的NSLock就属于互斥锁

读写锁

对于某一资源同时只允许有一个写访问或者多个读访问,iOS中pthread_rwlock就是读写锁

条件锁

在满足某个条件的时候进行加锁或者解锁,iOS中可使用NSConditionLock来实现

递归锁
  1. 可以被一个线程多次获得,而不会引起死锁。
  2. 它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。
  3. 只有当所有的锁被释放之后,其他线程才可以获得锁,iOS可使用NSRecursiveLock来实现

自旋锁和互斥锁的区别是什么?什么情况下使用?

区别
  • 自旋锁会忙等:所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。
    • atomic、OSSpinLock、dispatch_semaphore_t
  • 互斥锁会休眠:所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁,此时会唤醒休眠线程
    • pthread_mutex、@synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock
什么情况下使用?
  • 什么情况使用自旋锁比较划算?
    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)经常被调用,但竞争情况很少发生
    • CPU资源不紧张
    • 多核处理器
  • 什么情况使用互斥锁比较划算?
    • 预计线程等待锁的时间较长
    • 单核处理器
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈

iOS中的锁:

OSSpinLock (iOS10 废弃)
  • OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源

  • 目前已经不再安全,可能会出现优先级反转问题

    在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁

  • 建议不要使用OSSpinLock,用os_unfair_lock来代替

    #import <libkern/OSAtomic.h>
    // 初始化
    OSSpinLock lock = OS_SPINLOCK_INIT;
    // 尝试加锁(如果需要等待就不加锁,直接返回false,如果不需要等待就加锁,返回true)
    bool result = OSSpinLockTry(&lock);
    // 加锁
    OSSpinLockLock(&lock);
    // 解锁
    OSSpinLockUnlock(&lock);
    
os_unfair_lock ( iOS10 开始)
  • os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持

  • 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等

    #import <os/lock.h>
    // 初始化
    os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
    // 尝试加锁
    os_unfair_lock_trylock(&lock);
    // 加锁
    os_unfair_lock_lock(&lock);
    // 解锁
    os_unfair_lock_unlock(&lock);
    
pthread_mutex 跨平台的锁,使用步骤复杂,不建议
  • mutex叫做”互斥锁”,等待锁的线程会处于休眠状态

    #import <pthread.h>
    // 初始化锁的属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutextattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    //初始化锁
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, &attr);
    // 尝试加锁
    pthread_mutex_trylock(&mutex);
    // 加锁
    pthread_mutex_lock(&mutext);
    //解锁
    pthread_mutext_unlock(&mutex);
    //销毁锁资源
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_destroy(&mutext);
    
  • pthread_mutex 递归锁

    // 递归锁:允许同一个线程对一把锁进行重复加锁
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_init(mutex, &attr);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
  • pthread_mutex条件锁

    //初始化锁
    pthread_mutex_t mutex;
    // NULL 代表使用默认属性
    pthread_mutex_init(&mutex, NULL);
    // 初始化条件
    pthread_cond_t condition;
    pthread_cond_init(&condition, NULL);
    // 等待条件(进入休眠,放开mutex锁,被唤醒后,会再次对mutext加锁)
    pthread_cond_wait(&condition, &mutex);
    // 激活一个等待条件的线程
    pthread_cond_signal(&condition);
    // 激活所有等待条件的线程
    pthread_cond_boradcast(&conditioin);
    // 销毁资源
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&condition);
    
dispatch_semaphore 建议使用,性能也比较好
  • semaphore叫做”信号量”

  • 信号量的初始值,可以用来控制线程并发访问的最大数量

  • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

    // 信号量的初始值
    int value = 1;
    // 初始化信号量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
    // 如果信号量<=0,当前线程就会进入休眠等待(直到信号量>0)
    // 如果信号量>0,就-1,然后往下执行后面的代码
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // 让信号量的值+1
    dispatch_semaphore_signal(semaphore);
    
dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行队列
  • 直接使用GCD的串行队列,也是可以实现线程同步的

    dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{});
    
NSLock 对 mutex 封装
  • NSLock是对mutex普通锁的封装
NSRecursiveLock 递归锁,用于多层嵌套的锁
  • NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition
  • NSCondition是对mutex和cond的封装
NSConditionLock
  • NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
@synchronized 性能最差
  • @synchronized是对mutex递归锁的封装
  • @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
  • 源码查看:objc4中的objc-sync.mm文件

性能高低

性能从高到低排序
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized

如何理解死锁

死锁简述

通常指有两个线程T1和T2都卡住了,并等待对方完成某些操作。T1不能完成是因为它在等待T2完成。但T2也不能完成,因为它在等待T1完成。于是大家都完不成,就导致了死锁(DeadLock)

产生死锁的四个必要条件

互斥条件

一个资源每次只能被一个线程使用

请求保持条件

一个线程因请求资源而阻塞时,对已获得的资源保持不放

不剥夺条件

线程已获得的资源,在未使用完之前,不能强行剥夺

循环等待条件

若干线程程之间形成一种头尾相接的循环等待资源关系

如何检测死锁

juejin.cn/post/703745…

mp.weixin.qq.com/s?__biz=Mzg…

先检测卡顿,在卡顿中怀疑死循环和死锁

pthread、NSThread、GCD、NSOperationQueue(一图展示)

objc_multithread.png