iOS之多线程

123 阅读9分钟

和谐学习!不急不躁!!我是你们的老朋友小青龙~

概念

什么是程序

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

什么是线程

线程是独立运行和独立调度的基本单位,不拥有资源。一个进程可能包含多个(至少拥有一个)线程,而线程是分配资源的基本单位,故而线程的开销更小、可以更好的提高程序执行的并发效果。

线程的几种状态

  • 新建状态:创新新的线程对象。
  • 就绪状态:其它线程调用了该线程对象的start()方法。
  • 运行状态:获得cpu权限,并执行相应代码。
  • 阻塞状态:失去cpu权限,暂停运行。
  • 死亡状态:线程执行完了或异常退出了run()方法。

状态对应伪代码如下: image.png

拓展:我们常说线程作不拥有资源,即没有存储空间,那么tls又是什么呢?
  • tls又名线程局部存储,它不属于线程,它是操作系统为线程单独开辟的小存储空间。
  • tls的作用是将数据和正在运行的指定线程关联起来。
  • 由于进程的全局变量和函数内的静态变量是所有线程可以访问的,那么一旦其中一个线程出错,就会导致所有线程崩溃。tls的出现则很好的结局了这个问题,在一张全局表里,通过不同线程的ID查找来区分每个线程的数据。

什么是进程

  • `进程是指一段程序的执行过程。
  • 进程是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 进程有三个状态,就绪、运行和阻塞。

什么是多线程

多线程是一种提高资源利用率而引发的一个概念。简单来说,就是原本你要做5件事情,抛去多线程的概念来说,你需要一件事情做完,才能开始做另外一件。但是多线程可以让你把5件事情一起做。

这里引发一个概念:多线程并行、多线程并发

  • 单核处理器:多线程就是任务之间的快速切换(A任务做一点,然后快速切换到B任务做一点,然后快速切换到C任务)。

image.png

  • 多核处理器:多个任务一起进行,不需要任务切换。

image.png

什么是线程安全

执行多线程之后,在不需要跟主线程做任何同步的情况下,最终的运行结果符合我们的预期效果,我们称之为「线程安全」。否则,超出了预期效果就是「非线程安全」。

什么是线程池

在一个应用程序运行过程中,我们需要不停的创建与销毁线程,这样对内存的消耗很大。所以引出了线程池的概念。顾名思义,线程池的作用就是管理线程的,减少内存消耗。线程池的概念:

  • corePoolSize:核心线程数量

  • maximumPoolSize:可以容纳的最大线程的数量

  • keepAliveTime:除了核心线程之外的其他的最长可以保留的时间

  • unit:计算这个时间的一个单位

  • workQueue:等待队列(执行的原则是先进先出)

  • threadFactory:创建线程的线程工厂

  • handler:拒绝策略(可以在任务满了之后,拒绝执行某些任务)

在线程池中,核心线程在无论是否有任务都不能被清除,其余的都是有存活时间的。

什么是饱和策略

首先我们来看下线程池的执行流程:

image.png

从图中我们看到,当线程池里的核心线程池、队列、线程池都满了的前提下,有新的任务进来,就会进入到一个策略模式(饱和策略)。这种策略又分为四种:

  • AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满

  • DisCardPolicy:不执行新任务,也不抛出异常

  • DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行

  • CallerRunsPolicy:直接调用execute来执行当前任务

多线程带来的影响

随着多核计算机的普及,线程为某些类型的应用程序提供了一种提高性能的方法。执行不同任务的线程可以在不同的处理器内核上同时执行,从而使应用程序能够在给定的时间内增加所做的工作量。

线程带来的好处同时也带来了「潜在的问题」。由于单个应用程序中的线程共享相同的内存空间,因此它们可以访问所有相同的数据结构。如果两个线程试图同时操作同一数据结构,那么一个线程可能会以破坏结果数据结构的方式覆盖另一个线程的更改。即使有适当的保护措施,您仍然必须注意编译器优化会在代码中引入微妙。

多线程的几种方式

关于多线程的几种实现方案,网上找了张图供大家参考:

image.png

NSThread

使用的两种方式:

  • detachNewThreadSelector:toTarget:withObject:
[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];
  • 创建新的NSThread对象并调用其「start」方法。(仅在iOS和OS X v10.5及更高版本中受支持。)
NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                        selector:@selector(myThreadMainMethod:)
                                        object:nil];
[myThread start];  // Actually create the thread

第一种类方法的创建方式比较古板,只能在线程运行后从线程本身执行此操作。

第二种创建NSThread实例对象的方法,无需立即生成相应的新线程,能够在启动线程之前获取和设置各种线程属性。它还可以在以后使用该线程对象引用正在运行的线程。

相比起第一种方式,第二种灵活性更高。

自旋锁和互斥锁

定义

自旋锁:当前线程执行状态下,以死循环的方式频繁的访问当前线程是否已经执行完,随着访问的时间越来越长,性能的消耗也是直线上升。

互斥锁:共享资源被其它线程加锁了,任务就进入就绪等待状态,直到被加锁的资源解锁,线程才会被激活。相对来说,性能消耗小一些。

相对来说,「自旋锁」比「互斥锁」效率更高

哪些地方有用到「自旋锁」呢?

image.png

我们平时用的copy修饰的属性,会产生一个叫objc_setProperty的setter方法,我们来看下objc_setProperty的底层实现:

image.png

进入reallySetProperty

image.png

锁的其它知识点

关于锁的其它知识点,这里就不继续阐述了,这里推荐一篇其它博主写的文章供大家参考: iOS开发中自旋和互斥锁的理解以及所有锁的性能比较

GCD

GCD即Grand Central Dispatch,是Apple公司开发的一个多核编程的解决方案。针对多核手机,实现线程上的并发执行任务。

GCD总的来说就是:将任务添加到队列,并指定任务要执行的函数

根据任务的执行顺序,我们把队列分为「串行队列」和「并行队列」。

根据任务是否堵塞当前线程,我们可以把任务执行分为「同步执行」和「异步执行」。

  • 串行队列:按顺序一个接一个的执行任务。

  • 并行队列:允许多个任务同时执行。

  • 同步执行:任务创建后执行完才能往下走,不具备开启新线程的能力。

  • 异步执行:任务创建后,可以晚点执行,且具备开启新线程的能力。

听起来似乎有点重复,尤其是「串行队列」和「同步执行」似乎是一个意思。但是我们要区分一点,队列只负责调度,具体怎么执行要看是「异步执行」还是「同步执行」。

这样看来,两两组合就凑成了4种情况:

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

我们接下来一个个分析:

  • 并行队列 + 异步执行 并行队列允许多个任务同时执行,异步执行让多个任务同时执行。

image.png

再运行一次:

image.png

改下代码,验证同时执行:

image.png

  • 并行队列 + 同步执行

并行队列允许多个任务同时执行,同步执行要一个接一个执行,所以按顺序打印。

image.png

  • 串行队列 + 异步执行

串行队列按顺序调度,依次执行任务2、任务3、任务4。

image.png

  • 串行队列 + 同步执行

串行队列按顺序调度,同步执行让任务一个执行完了才开始执行下一个任务。

image.png

关于这些概念,这边提供了两张图供大家理解:

image.png

image.png

iOS面试题 - 多线程

题目一:影响任务执行速度的因素有哪些?

答:

  • cpu调度能力

  • 线程的状态

  • 任务的优先级

  • 任务的复杂度

举个例子:做作业,暑假来了,各个科目都布置了作业(作业 = 任务)

  • cpu调度能力:即学生的个人能力,学霸完成一本暑假作业只需要一天,学渣可能要写七天七夜;
  • 线程的状态:即作业本身写着写着突然暂停,比如奶奶拿着一篮子草莓过来让学生先停下,吃完了再继续做作业,导致学生在做这道题目的时候停留了好久。
  • 线程的优先级:数学、语文、英语,男同学普遍觉得数学最简单,所以数学先做,数学也是最先完成的一个作业(最先完成的任务)。
  • 任务的复杂度:数学、语文、英语,最复杂的应该是英语(当然你也可以觉得是数学),所以英语这个作业完成的特别慢。

影响任务优先级的因素

  • 用户指定( NSThread有个属性「qualityOfService」)
  • 任务等待的时长(可能被提升优先级)
  • 任务长时间不执行(可能被降低优先级)

关于线程,如果想了解其它方面的内容,可以查阅文档

题目二:下面这段代码运行结果是什么?(有关死锁)

- (void)demo{
        
        dispatch_queue_t queue = dispatch_queue_create("ssj_SERIAL", DISPATCH_QUEUE_SERIAL);
        NSLog(@"1");
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_sync(queue, ^{
                NSLog(@"3");
            });
            NSLog(@"4");
        });
        NSLog(@"5");
    }

答:会报错,原因是线程死锁

解析:

WX20210915-154334@2x.png

实际上,dispatch_sync堵塞的是dispatch_async这个代码块的线程,要想让这段代码正常执行,我们只需要在创建队列的时候,把DISPATCH_QUEUE_SERIAL改成DISPATCH_QUEUE_CONCURRENT即可(串行队列该改成并行队列)。